From 5c7472eab63d198b7d2ec01f98b8b05146afe16c Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Mon, 13 Apr 2026 11:16:46 +0200 Subject: [PATCH 1/3] tests: ignore env vars and ~/.skitter config when running docker e2e --- tests/conftest.py | 20 ++++++++++++++++++++ tests/test_docker_e2e.py | 3 +++ 2 files changed, 23 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index fa0d812..caab752 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,12 +8,32 @@ import asyncio import json +import os import socket import subprocess import sys +import tempfile import uuid from pathlib import Path +# Isolate tests from the developer's ~/.skitter/ config and from MQTT_*/SKITTER_* +# values uv and skitter.mqtt pull out of the project .env. Point SKITTER_HOME at +# a throwaway dir and blank the relevant env vars so load_config() falls back to +# built-in defaults (localhost:1883, no auth, org=skitter unit=default), matching +# what CI sees on a fresh runner. Set to empty string rather than popping so +# skitter.mqtt's load_dotenv() call at import time leaves them alone (dotenv +# respects already-set vars). Must happen before importing anything from skitter.*. +os.environ["SKITTER_HOME"] = tempfile.mkdtemp(prefix="skitter-test-") +for _var in ( + "MQTT_BROKER_URL", + "MQTT_USERNAME", + "MQTT_PASSWORD", + "MQTT_CA_CERT", + "SKITTER_A2A_ORG", + "SKITTER_A2A_UNIT", +): + os.environ[_var] = "" + import aiomqtt import pytest diff --git a/tests/test_docker_e2e.py b/tests/test_docker_e2e.py index d8c57d7..55c6229 100644 --- a/tests/test_docker_e2e.py +++ b/tests/test_docker_e2e.py @@ -97,6 +97,9 @@ def host_env(tmp_path): """Fresh isolated environment for each test that runs skitter CLI commands.""" env = os.environ.copy() + # conftest pins SKITTER_HOME for in-process test code; subprocess tests here + # want skitter to derive its home from HOME, so drop the inherited override. + env.pop("SKITTER_HOME", None) env["HOME"] = str(tmp_path) env["MQTT_BROKER_URL"] = "mqtt://localhost:1883" env["NO_COLOR"] = "1" From 5dc71714587042d51535dcb81c0e34a968aeec94 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Mon, 13 Apr 2026 14:41:18 +0200 Subject: [PATCH 2/3] ci: fix docker e2e tests --- .github/workflows/ci-docker-e2e.yml | 4 +++ skitter/agent_runner.py | 54 ++++++++++++++--------------- tests/conftest.py | 10 +++--- tests/unit/test_agent_runner.py | 6 ++-- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci-docker-e2e.yml b/.github/workflows/ci-docker-e2e.yml index b47c69d..620d792 100644 --- a/.github/workflows/ci-docker-e2e.yml +++ b/.github/workflows/ci-docker-e2e.yml @@ -9,6 +9,10 @@ env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} SKITTER_LLM_API_KEY: ${{ secrets.SKITTER_LLM_API_KEY }} SKITTER_LLM_MODEL: ${{ vars.SKITTER_LLM_MODEL || 'claude-haiku-4-5' }} + # CI runners are slower than dev machines; large prompts to the Claude CLI can + # take well over the 30s default before the agent emits its first artifact. + # Keep below the 120s outer timeout in send_and_collect to avoid racing it. + SKITTER_STREAM_IDLE_TIMEOUT: "90" jobs: docker-e2e: diff --git a/skitter/agent_runner.py b/skitter/agent_runner.py index c55cace..234729a 100644 --- a/skitter/agent_runner.py +++ b/skitter/agent_runner.py @@ -114,34 +114,34 @@ def _build_cli_cmd( if agent.instructions: cmd.extend(["-c", f"developer_instructions={agent.instructions}"]) cmd.append(prompt) + return cmd + + full_prompt = prompt + if agent.instructions: + full_prompt = f"{agent.instructions}\n\n{prompt}" + cmd = [ + "claude", + "-p", + full_prompt, + "--output-format", + "stream-json", + "--verbose", + ] + if resume_id: + cmd.extend(["--resume", resume_id]) else: - full_prompt = prompt - if agent.instructions: - full_prompt = f"{agent.instructions}\n\n{prompt}" - cmd = [ - "claude", - "-p", - full_prompt, - "--output-format", - "stream-json", - "--verbose", - ] - if resume_id: - cmd.extend(["--resume", resume_id]) - elif _PERMISSION_MODE == "bypassPermissions": - cmd.extend( - ["--dangerously-skip-permissions", "--settings", _SANDBOX_SETTINGS] - ) - else: - cmd.extend( - ["--permission-mode", _PERMISSION_MODE, "--settings", _SANDBOX_SETTINGS] - ) - if agent.model: - cmd.extend(["--model", agent.model]) - if agent.max_turns: - cmd.extend(["--max-turns", str(agent.max_turns)]) - if agent.tools: - cmd.extend(["--allowedTools", ",".join(agent.tools)]) + # Use --permission-mode rather than --dangerously-skip-permissions even for + # bypassPermissions: the latter triggers a Claude CLI hang on large inputs + # combined with --output-format stream-json. + cmd.extend( + ["--permission-mode", _PERMISSION_MODE, "--settings", _SANDBOX_SETTINGS] + ) + if agent.model: + cmd.extend(["--model", agent.model]) + if agent.max_turns: + cmd.extend(["--max-turns", str(agent.max_turns)]) + if agent.tools: + cmd.extend(["--allowedTools", ",".join(agent.tools)]) return cmd diff --git a/tests/conftest.py b/tests/conftest.py index caab752..9fdbf8b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,10 +34,10 @@ ): os.environ[_var] = "" -import aiomqtt -import pytest +import aiomqtt # noqa: E402 — after env scrubbing above +import pytest # noqa: E402 -from skitter.a2a import ( +from skitter.a2a import ( # noqa: E402 a2a_org, a2a_unit, A2ARequest, @@ -49,8 +49,8 @@ topic_discovery, topic_reply, ) -from skitter.config import load_config as _load_config -from skitter.mqtt import mqtt_client_kwargs +from skitter.config import load_config as _load_config # noqa: E402 +from skitter.mqtt import mqtt_client_kwargs # noqa: E402 PROJECT_ROOT = Path(__file__).parent.parent diff --git a/tests/unit/test_agent_runner.py b/tests/unit/test_agent_runner.py index ad5ca61..f53663e 100644 --- a/tests/unit/test_agent_runner.py +++ b/tests/unit/test_agent_runner.py @@ -161,8 +161,10 @@ def test_build_claude_cmd(self): assert "--agent" not in cmd assert "--model" in cmd assert "sonnet" in cmd - assert "--dangerously-skip-permissions" in cmd - assert "--permission-mode" not in cmd + # Use --permission-mode bypassPermissions: --dangerously-skip-permissions + # triggers a Claude CLI hang on large + stream-json inputs. + assert "--dangerously-skip-permissions" not in cmd + assert cmd[cmd.index("--permission-mode") + 1] == "bypassPermissions" def test_build_codex_cmd(self): from skitter.agent_runner import _build_cli_cmd From e25d843daf9743d81fddddd1018c081eabcdeff9 Mon Sep 17 00:00:00 2001 From: Ivan Dyachkov Date: Mon, 13 Apr 2026 15:00:50 +0200 Subject: [PATCH 3/3] ci: fix acceptance tests --- tests/test_acceptance.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 39d33a8..c75131e 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -163,6 +163,9 @@ def mock_codex_bin(tmp_home): def skitter_env(tmp_home, mock_claude_bin, mock_codex_bin): """Subprocess environment with isolated HOME and mock CLIs on PATH.""" env = os.environ.copy() + # conftest pins SKITTER_HOME for in-process test code; subprocess tests here + # want skitter to derive its home from HOME, so drop the inherited override. + env.pop("SKITTER_HOME", None) env["HOME"] = str(tmp_home) env["PATH"] = f"{mock_claude_bin}:{mock_codex_bin}:{env.get('PATH', '')}" env["NO_COLOR"] = "1"