Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci-docker-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
54 changes: 27 additions & 27 deletions skitter/agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
32 changes: 26 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,36 @@

import asyncio
import json
import os
import socket
import subprocess
import sys
import tempfile
import uuid
from pathlib import Path

import aiomqtt
import pytest

from skitter.a2a import (
# 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 # noqa: E402 — after env scrubbing above
import pytest # noqa: E402

from skitter.a2a import ( # noqa: E402
a2a_org,
a2a_unit,
A2ARequest,
Expand All @@ -29,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

Expand Down
3 changes: 3 additions & 0 deletions tests/test_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions tests/test_docker_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 4 additions & 2 deletions tests/unit/test_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading