steerdev 1.0.57__tar.gz → 1.0.59__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {steerdev-1.0.57 → steerdev-1.0.59}/PKG-INFO +1 -1
- {steerdev-1.0.57 → steerdev-1.0.59}/pyproject.toml +1 -1
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/agent_loop.py +55 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/commands.py +1 -1
- steerdev-1.0.59/src/steerdev_agent/api/merger.py +71 -0
- steerdev-1.0.59/tests/test_workflow_runs_api.py +358 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/.github/workflows/pre-commit.yml +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/.github/workflows/publish.yml +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/.gitignore +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/.pre-commit-config.yaml +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/AGENTS.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/CLAUDE.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/README.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/scripts/pre-commit-version-bump.sh +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/snapshots/steerdev-agent-v1/Dockerfile +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/snapshots/steerdev-agent-v1/start-agent.sh +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/__init__.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/__init__.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/activity.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/agents.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/canals.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/client.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/configs.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/context.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/events.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/hooks.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/implementation_plan.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/messages.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/prd.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/reports.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/runs.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/sessions.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/specs.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/tasks.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/workflow_runs.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/api/workflows.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/cli.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/config/__init__.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/config/models.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/config/platform.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/config/settings.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/evidence.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/evidence_assets.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/executor/__init__.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/executor/base.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/executor/claude.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/executor/stream.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/handlers/__init__.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/handlers/prd.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/integration.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/prompt/__init__.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/prompt/builder.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/prompt/templates.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/prompt/workflow_template.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/py.typed +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/retry.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/runner.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/__init__.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/claude_setup.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/repo_setup.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/ci/canal-integration.yml +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/claude_md_section.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/settings.json +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/skills/steerdev-activity-skill/SKILL.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/skills/steerdev-canal-workflow-skill/SKILL.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/skills/steerdev-context-skill/SKILL.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/skills/steerdev-git-workflow-skill/SKILL.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/skills/steerdev-merge-into-canal-skill/SKILL.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/skills/steerdev-progress-logging-skill/SKILL.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/skills/steerdev-single-task-merge-skill/SKILL.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/skills/steerdev-specs-management-skill/SKILL.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/skills/steerdev-task-management-skill/SKILL.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/skills/steerdev-wave-tasks-merge-skill/SKILL.md +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/steerdev.yaml +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/worktrunk.config.toml +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/update_check.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/version.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/workflow/__init__.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/workflow/context.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/workflow/executor.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/workflow/memory.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/workspace/__init__.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/workspace/project_manager.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/workspace/tool_detection.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/worktree.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/__init__.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_agent_loop.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_agent_loop_extended.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_agents_api.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_api_client.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_claude_executor.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_claude_setup.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_client_methods.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_commands_api.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_config.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_config_extended.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_conflict_mitigation.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_context_search.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_executor.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_platform_config.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_prompt.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_reports_client.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_retry.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_runner_merge_modes.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_runner_worktrees.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_stream_parser.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_tasks.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_version.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_workflow_context.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_workflow_memory.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_workflow_prompt_template.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_workspace.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_workspace_extended.py +0 -0
- {steerdev-1.0.57 → steerdev-1.0.59}/tests/test_worktree.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: steerdev
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.59
|
|
4
4
|
Summary: Backend task runner for steerdev.com - orchestrates CLI coding agents with activity reporting
|
|
5
5
|
Project-URL: Homepage, https://github.com/pentoai/steerdev-agent
|
|
6
6
|
Project-URL: Repository, https://github.com/pentoai/steerdev-agent
|
|
@@ -126,6 +126,8 @@ class CommandExecutor:
|
|
|
126
126
|
await self._execute_task_command(command, project_id, working_directory)
|
|
127
127
|
elif command.command_type == "prompt":
|
|
128
128
|
await self._execute_prompt_command(command, project_id, working_directory)
|
|
129
|
+
elif command.command_type == "merger":
|
|
130
|
+
await self._execute_merger_command(command, project_id, working_directory)
|
|
129
131
|
else:
|
|
130
132
|
await self._commands_client.mark_failed(
|
|
131
133
|
command.id, error=f"Unknown command type: {command.command_type}"
|
|
@@ -403,6 +405,59 @@ class CommandExecutor:
|
|
|
403
405
|
finally:
|
|
404
406
|
await events_client.close()
|
|
405
407
|
|
|
408
|
+
async def _execute_merger_command(
|
|
409
|
+
self,
|
|
410
|
+
command: CommandResponse,
|
|
411
|
+
project_id: str,
|
|
412
|
+
working_directory: Path,
|
|
413
|
+
) -> None:
|
|
414
|
+
"""Execute a merger command - triggers merger run and polls for completion."""
|
|
415
|
+
from steerdev_agent.api.merger import MergerClient
|
|
416
|
+
|
|
417
|
+
assert self._commands_client is not None
|
|
418
|
+
|
|
419
|
+
merger_run_id = command.metadata.get("mergerRunId")
|
|
420
|
+
if not merger_run_id:
|
|
421
|
+
raise ValueError("merger command missing mergerRunId in metadata")
|
|
422
|
+
|
|
423
|
+
merger_client = MergerClient(api_key=self._api_key)
|
|
424
|
+
|
|
425
|
+
logger.info(f"Starting merger run: {merger_run_id}")
|
|
426
|
+
|
|
427
|
+
# Mark command as executing
|
|
428
|
+
await self._commands_client.mark_executing(command.id)
|
|
429
|
+
|
|
430
|
+
# Trigger execution
|
|
431
|
+
result = merger_client.execute_merger_run(merger_run_id)
|
|
432
|
+
if not result:
|
|
433
|
+
raise RuntimeError(f"Failed to trigger merger run {merger_run_id}")
|
|
434
|
+
|
|
435
|
+
# Poll for completion
|
|
436
|
+
max_polls = 60 # 30 minutes at 30s intervals
|
|
437
|
+
for _ in range(max_polls):
|
|
438
|
+
run = merger_client.get_merger_run(merger_run_id)
|
|
439
|
+
if not run:
|
|
440
|
+
raise RuntimeError("Failed to get merger run status")
|
|
441
|
+
|
|
442
|
+
if run.status in ("completed", "failed", "partial"):
|
|
443
|
+
logger.info(f"Merger run {merger_run_id} finished: {run.status}")
|
|
444
|
+
if run.status == "failed":
|
|
445
|
+
raise RuntimeError(f"Merger run failed: {run.error_message}")
|
|
446
|
+
await self._commands_client.mark_completed(
|
|
447
|
+
command.id,
|
|
448
|
+
result=f"Merger run {merger_run_id} {run.status}",
|
|
449
|
+
)
|
|
450
|
+
self._commands_succeeded += 1
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
if self._shutdown_event.is_set():
|
|
454
|
+
logger.info("Shutdown during merger polling, aborting")
|
|
455
|
+
raise RuntimeError("Shutdown requested during merger run")
|
|
456
|
+
|
|
457
|
+
await asyncio.sleep(30)
|
|
458
|
+
|
|
459
|
+
raise RuntimeError(f"Merger run {merger_run_id} timed out")
|
|
460
|
+
|
|
406
461
|
|
|
407
462
|
# ---------------------------------------------------------------------------
|
|
408
463
|
# Project-scoped agent (original)
|
|
@@ -13,7 +13,7 @@ from steerdev_agent.api.client import get_agent_id, get_api_endpoint, get_api_ke
|
|
|
13
13
|
|
|
14
14
|
console = Console()
|
|
15
15
|
|
|
16
|
-
CommandType = Literal["task", "prompt", "control"]
|
|
16
|
+
CommandType = Literal["task", "prompt", "control", "merger"]
|
|
17
17
|
CommandStatus = Literal[
|
|
18
18
|
"pending", "claimed", "executing", "completed", "failed", "cancelled", "expired"
|
|
19
19
|
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Merger API client for merger agent operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from steerdev_agent.api.client import SteerDevClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MergerRunResponse(BaseModel):
|
|
11
|
+
id: str
|
|
12
|
+
status: str
|
|
13
|
+
wave_ids: list[str] = []
|
|
14
|
+
task_ids: list[str] = []
|
|
15
|
+
pr_ids: list[str] = []
|
|
16
|
+
evidence_report_ids: list[str] = []
|
|
17
|
+
summary: str | None = None
|
|
18
|
+
merged_branches: list[str] | None = None
|
|
19
|
+
error_message: str | None = None
|
|
20
|
+
started_at: str | None = None
|
|
21
|
+
completed_at: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GateResponse(BaseModel):
|
|
25
|
+
id: str
|
|
26
|
+
merger_run_id: str
|
|
27
|
+
canal_id: str
|
|
28
|
+
gate_number: int
|
|
29
|
+
status: str
|
|
30
|
+
started_at: str | None = None
|
|
31
|
+
completed_at: str | None = None
|
|
32
|
+
details: dict[str, Any] | None = None
|
|
33
|
+
error_message: str | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MergerClient(SteerDevClient):
|
|
37
|
+
"""Client for merger agent API operations."""
|
|
38
|
+
|
|
39
|
+
def execute_merger_run(self, merger_run_id: str) -> dict[str, Any] | None:
|
|
40
|
+
"""Trigger execution of a full merger run."""
|
|
41
|
+
response = self.post(f"/merger-runs/{merger_run_id}/execute")
|
|
42
|
+
if response and response.status_code == 200:
|
|
43
|
+
return response.json()
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
def get_merger_run(self, merger_run_id: str) -> MergerRunResponse | None:
|
|
47
|
+
"""Get merger run status."""
|
|
48
|
+
response = self.get(f"/merger-runs/{merger_run_id}")
|
|
49
|
+
if response and response.status_code == 200:
|
|
50
|
+
return MergerRunResponse(**response.json())
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
def get_merger_gates(self, merger_run_id: str) -> list[GateResponse]:
|
|
54
|
+
"""Get gate statuses for a merger run."""
|
|
55
|
+
response = self.get(f"/merger-runs/{merger_run_id}/gates")
|
|
56
|
+
if response and response.status_code == 200:
|
|
57
|
+
data = response.json()
|
|
58
|
+
return [GateResponse(**g) for g in data.get("gates", [])]
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
def execute_gate(
|
|
62
|
+
self, canal_id: str, gate_number: int, merger_run_id: str, merge_id: str | None = None
|
|
63
|
+
) -> dict[str, Any] | None:
|
|
64
|
+
"""Trigger execution of a specific gate."""
|
|
65
|
+
payload: dict[str, Any] = {"merger_run_id": merger_run_id}
|
|
66
|
+
if merge_id:
|
|
67
|
+
payload["merge_id"] = merge_id
|
|
68
|
+
response = self.post(f"/canals/{canal_id}/gates/{gate_number}", json=payload)
|
|
69
|
+
if response and response.status_code == 200:
|
|
70
|
+
return response.json()
|
|
71
|
+
return None
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Tests for workflow runs API client — models, start_workflow, and disabled-workflow handling."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
from unittest.mock import AsyncMock, patch
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from steerdev_agent.api.workflow_runs import (
|
|
11
|
+
PhaseRunResponse,
|
|
12
|
+
WorkflowRunResponse,
|
|
13
|
+
WorkflowRunsClient,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# ── Shared fixtures ───────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
NOW = "2026-01-01T00:00:00Z"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _make_workflow_run_json(
|
|
22
|
+
*,
|
|
23
|
+
workflow_id: str = "wf-1",
|
|
24
|
+
workflow_name: str = "Feature Dev",
|
|
25
|
+
status: str = "running",
|
|
26
|
+
total_phases: int = 3,
|
|
27
|
+
**extra: Any,
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
return {
|
|
30
|
+
"id": "wr-1",
|
|
31
|
+
"workflow_id": workflow_id,
|
|
32
|
+
"workflow_name": workflow_name,
|
|
33
|
+
"status": status,
|
|
34
|
+
"total_phases": total_phases,
|
|
35
|
+
"created_at": NOW,
|
|
36
|
+
"updated_at": NOW,
|
|
37
|
+
**extra,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── Model tests ───────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestWorkflowRunModels:
|
|
45
|
+
"""Tests for Pydantic models in workflow_runs.py."""
|
|
46
|
+
|
|
47
|
+
def test_workflow_run_response_minimal(self):
|
|
48
|
+
resp = WorkflowRunResponse(
|
|
49
|
+
id="wr-1",
|
|
50
|
+
workflow_id="wf-1",
|
|
51
|
+
workflow_name="Test",
|
|
52
|
+
status="running",
|
|
53
|
+
created_at=NOW,
|
|
54
|
+
updated_at=NOW,
|
|
55
|
+
)
|
|
56
|
+
assert resp.id == "wr-1"
|
|
57
|
+
assert resp.project_id is None
|
|
58
|
+
assert resp.phase_runs is None
|
|
59
|
+
assert resp.phases_completed == 0
|
|
60
|
+
assert resp.total_phases == 0
|
|
61
|
+
|
|
62
|
+
def test_workflow_run_response_full(self):
|
|
63
|
+
resp = WorkflowRunResponse(
|
|
64
|
+
id="wr-1",
|
|
65
|
+
workflow_id="wf-1",
|
|
66
|
+
workflow_name="Feature Dev",
|
|
67
|
+
project_id="proj-1",
|
|
68
|
+
run_id="run-1",
|
|
69
|
+
linear_issue_id="LIN-123",
|
|
70
|
+
status="completed",
|
|
71
|
+
current_phase_id="p3",
|
|
72
|
+
current_phase_name="Verify",
|
|
73
|
+
context={"key": "value"},
|
|
74
|
+
phases_completed=3,
|
|
75
|
+
total_phases=3,
|
|
76
|
+
started_at=NOW,
|
|
77
|
+
completed_at=NOW,
|
|
78
|
+
created_at=NOW,
|
|
79
|
+
updated_at=NOW,
|
|
80
|
+
)
|
|
81
|
+
assert resp.phases_completed == 3
|
|
82
|
+
assert resp.context == {"key": "value"}
|
|
83
|
+
assert resp.linear_issue_id == "LIN-123"
|
|
84
|
+
|
|
85
|
+
def test_phase_run_response_minimal(self):
|
|
86
|
+
resp = PhaseRunResponse(
|
|
87
|
+
id="pr-1",
|
|
88
|
+
workflow_run_id="wr-1",
|
|
89
|
+
phase_id="p-1",
|
|
90
|
+
phase_name="Plan",
|
|
91
|
+
phase_type="plan",
|
|
92
|
+
status="pending",
|
|
93
|
+
created_at=NOW,
|
|
94
|
+
updated_at=NOW,
|
|
95
|
+
)
|
|
96
|
+
assert resp.attempt_number == 1
|
|
97
|
+
assert resp.input_context == {}
|
|
98
|
+
assert resp.result_summary is None
|
|
99
|
+
|
|
100
|
+
def test_phase_run_response_full(self):
|
|
101
|
+
resp = PhaseRunResponse(
|
|
102
|
+
id="pr-1",
|
|
103
|
+
workflow_run_id="wr-1",
|
|
104
|
+
phase_id="p-1",
|
|
105
|
+
phase_name="Implement",
|
|
106
|
+
phase_type="implement",
|
|
107
|
+
status="completed",
|
|
108
|
+
attempt_number=2,
|
|
109
|
+
input_context={"spec": "build API"},
|
|
110
|
+
output_context={"files": ["main.py"]},
|
|
111
|
+
result_summary="Implementation complete",
|
|
112
|
+
started_at=NOW,
|
|
113
|
+
completed_at=NOW,
|
|
114
|
+
created_at=NOW,
|
|
115
|
+
updated_at=NOW,
|
|
116
|
+
)
|
|
117
|
+
assert resp.attempt_number == 2
|
|
118
|
+
assert resp.output_context["files"] == ["main.py"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ── Client tests ──────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestWorkflowRunsClient:
|
|
125
|
+
"""Tests for WorkflowRunsClient."""
|
|
126
|
+
|
|
127
|
+
def test_init_with_api_key(self):
|
|
128
|
+
client = WorkflowRunsClient(api_key="test-key")
|
|
129
|
+
assert client.api_key == "test-key"
|
|
130
|
+
|
|
131
|
+
def test_init_from_env(self):
|
|
132
|
+
with patch.dict(os.environ, {"STEERDEV_API_KEY": "env-key"}):
|
|
133
|
+
client = WorkflowRunsClient()
|
|
134
|
+
assert client.api_key == "env-key"
|
|
135
|
+
|
|
136
|
+
def test_headers(self):
|
|
137
|
+
client = WorkflowRunsClient(api_key="test-key")
|
|
138
|
+
assert client.headers["Authorization"] == "Bearer test-key"
|
|
139
|
+
|
|
140
|
+
@pytest.mark.asyncio
|
|
141
|
+
async def test_context_manager(self):
|
|
142
|
+
async with WorkflowRunsClient(api_key="key") as client:
|
|
143
|
+
assert client.api_key == "key"
|
|
144
|
+
assert client._client is None
|
|
145
|
+
|
|
146
|
+
@pytest.mark.asyncio
|
|
147
|
+
async def test_close_without_client(self):
|
|
148
|
+
client = WorkflowRunsClient(api_key="key")
|
|
149
|
+
await client.close() # Should not raise
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TestStartWorkflow:
|
|
153
|
+
"""Tests for start_workflow — including disabled-workflow (409) handling."""
|
|
154
|
+
|
|
155
|
+
@pytest.mark.asyncio
|
|
156
|
+
async def test_start_workflow_success(self):
|
|
157
|
+
"""201 → returns WorkflowRunResponse."""
|
|
158
|
+
client = WorkflowRunsClient(api_key="key")
|
|
159
|
+
mock_response = httpx.Response(201, json=_make_workflow_run_json())
|
|
160
|
+
mock_http = AsyncMock()
|
|
161
|
+
mock_http.post = AsyncMock(return_value=mock_response)
|
|
162
|
+
client._client = mock_http
|
|
163
|
+
|
|
164
|
+
result = await client.start_workflow("wf-1", run_id="run-1")
|
|
165
|
+
assert result is not None
|
|
166
|
+
assert result.id == "wr-1"
|
|
167
|
+
assert result.workflow_id == "wf-1"
|
|
168
|
+
|
|
169
|
+
@pytest.mark.asyncio
|
|
170
|
+
async def test_start_workflow_with_initial_context(self):
|
|
171
|
+
"""Initial context is passed in the payload."""
|
|
172
|
+
client = WorkflowRunsClient(api_key="key")
|
|
173
|
+
mock_response = httpx.Response(201, json=_make_workflow_run_json())
|
|
174
|
+
mock_http = AsyncMock()
|
|
175
|
+
mock_http.post = AsyncMock(return_value=mock_response)
|
|
176
|
+
client._client = mock_http
|
|
177
|
+
|
|
178
|
+
result = await client.start_workflow(
|
|
179
|
+
"wf-1",
|
|
180
|
+
initial_context={"task_title": "Fix login bug"},
|
|
181
|
+
)
|
|
182
|
+
assert result is not None
|
|
183
|
+
# Verify payload included initial_context
|
|
184
|
+
call_kwargs = mock_http.post.call_args
|
|
185
|
+
payload = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json")
|
|
186
|
+
assert payload["initial_context"] == {"task_title": "Fix login bug"}
|
|
187
|
+
|
|
188
|
+
@pytest.mark.asyncio
|
|
189
|
+
async def test_start_workflow_server_error(self):
|
|
190
|
+
"""500 → returns None."""
|
|
191
|
+
client = WorkflowRunsClient(api_key="key")
|
|
192
|
+
mock_response = httpx.Response(500, text="Internal Server Error")
|
|
193
|
+
mock_http = AsyncMock()
|
|
194
|
+
mock_http.post = AsyncMock(return_value=mock_response)
|
|
195
|
+
client._client = mock_http
|
|
196
|
+
|
|
197
|
+
result = await client.start_workflow("wf-1")
|
|
198
|
+
assert result is None
|
|
199
|
+
|
|
200
|
+
@pytest.mark.asyncio
|
|
201
|
+
async def test_start_workflow_not_found(self):
|
|
202
|
+
"""404 → returns None."""
|
|
203
|
+
client = WorkflowRunsClient(api_key="key")
|
|
204
|
+
mock_response = httpx.Response(
|
|
205
|
+
404,
|
|
206
|
+
json={"error": "Not Found", "message": "Workflow not found"},
|
|
207
|
+
)
|
|
208
|
+
mock_http = AsyncMock()
|
|
209
|
+
mock_http.post = AsyncMock(return_value=mock_response)
|
|
210
|
+
client._client = mock_http
|
|
211
|
+
|
|
212
|
+
result = await client.start_workflow("wf-nonexistent")
|
|
213
|
+
assert result is None
|
|
214
|
+
|
|
215
|
+
@pytest.mark.asyncio
|
|
216
|
+
async def test_start_workflow_request_error(self):
|
|
217
|
+
"""Network error → returns None."""
|
|
218
|
+
client = WorkflowRunsClient(api_key="key")
|
|
219
|
+
mock_http = AsyncMock()
|
|
220
|
+
mock_http.post = AsyncMock(side_effect=httpx.RequestError("connection refused"))
|
|
221
|
+
client._client = mock_http
|
|
222
|
+
|
|
223
|
+
result = await client.start_workflow("wf-1")
|
|
224
|
+
assert result is None
|
|
225
|
+
|
|
226
|
+
@pytest.mark.asyncio
|
|
227
|
+
async def test_start_workflow_disabled_409_returns_none(self):
|
|
228
|
+
"""409 with NO_ACTIVE_WORKFLOW → returns None (agent should proceed without workflow)."""
|
|
229
|
+
client = WorkflowRunsClient(api_key="key")
|
|
230
|
+
mock_response = httpx.Response(
|
|
231
|
+
409,
|
|
232
|
+
json={
|
|
233
|
+
"error": "Workflow disabled",
|
|
234
|
+
"message": "The requested workflow is disabled and no suitable active workflow was found.",
|
|
235
|
+
"code": "NO_ACTIVE_WORKFLOW",
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
mock_http = AsyncMock()
|
|
239
|
+
mock_http.post = AsyncMock(return_value=mock_response)
|
|
240
|
+
client._client = mock_http
|
|
241
|
+
|
|
242
|
+
result = await client.start_workflow("wf-disabled")
|
|
243
|
+
assert result is None
|
|
244
|
+
|
|
245
|
+
@pytest.mark.asyncio
|
|
246
|
+
async def test_start_workflow_disabled_409_reassigned(self):
|
|
247
|
+
"""409 that re-triaged to a different workflow → 201 with new workflow_id.
|
|
248
|
+
|
|
249
|
+
This tests the server-side behavior: the API transparently re-assigns
|
|
250
|
+
to the best active workflow. The client receives a normal 201 with the
|
|
251
|
+
new workflow's run.
|
|
252
|
+
"""
|
|
253
|
+
client = WorkflowRunsClient(api_key="key")
|
|
254
|
+
# Server re-triaged to a different workflow
|
|
255
|
+
mock_response = httpx.Response(
|
|
256
|
+
201,
|
|
257
|
+
json=_make_workflow_run_json(
|
|
258
|
+
workflow_id="wf-fallback",
|
|
259
|
+
workflow_name="Bug Fix Workflow",
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
mock_http = AsyncMock()
|
|
263
|
+
mock_http.post = AsyncMock(return_value=mock_response)
|
|
264
|
+
client._client = mock_http
|
|
265
|
+
|
|
266
|
+
result = await client.start_workflow("wf-disabled-original")
|
|
267
|
+
assert result is not None
|
|
268
|
+
# The returned workflow_id is the re-assigned one, not the original
|
|
269
|
+
assert result.workflow_id == "wf-fallback"
|
|
270
|
+
assert result.workflow_name == "Bug Fix Workflow"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class TestAdvancePhase:
|
|
274
|
+
"""Tests for advance_phase."""
|
|
275
|
+
|
|
276
|
+
@pytest.mark.asyncio
|
|
277
|
+
async def test_advance_success(self):
|
|
278
|
+
client = WorkflowRunsClient(api_key="key")
|
|
279
|
+
mock_response = httpx.Response(
|
|
280
|
+
200,
|
|
281
|
+
json=_make_workflow_run_json(status="running"),
|
|
282
|
+
)
|
|
283
|
+
mock_http = AsyncMock()
|
|
284
|
+
mock_http.post = AsyncMock(return_value=mock_response)
|
|
285
|
+
client._client = mock_http
|
|
286
|
+
|
|
287
|
+
result = await client.advance_phase("wr-1", output_context={"files": ["a.py"]})
|
|
288
|
+
assert result is not None
|
|
289
|
+
assert result.status == "running"
|
|
290
|
+
|
|
291
|
+
@pytest.mark.asyncio
|
|
292
|
+
async def test_advance_failure(self):
|
|
293
|
+
client = WorkflowRunsClient(api_key="key")
|
|
294
|
+
mock_response = httpx.Response(500, text="error")
|
|
295
|
+
mock_http = AsyncMock()
|
|
296
|
+
mock_http.post = AsyncMock(return_value=mock_response)
|
|
297
|
+
client._client = mock_http
|
|
298
|
+
|
|
299
|
+
result = await client.advance_phase("wr-1")
|
|
300
|
+
assert result is None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class TestFailPhase:
|
|
304
|
+
"""Tests for fail_phase."""
|
|
305
|
+
|
|
306
|
+
@pytest.mark.asyncio
|
|
307
|
+
async def test_fail_phase_success(self):
|
|
308
|
+
client = WorkflowRunsClient(api_key="key")
|
|
309
|
+
mock_response = httpx.Response(
|
|
310
|
+
200,
|
|
311
|
+
json=_make_workflow_run_json(status="running"),
|
|
312
|
+
)
|
|
313
|
+
mock_http = AsyncMock()
|
|
314
|
+
mock_http.post = AsyncMock(return_value=mock_response)
|
|
315
|
+
client._client = mock_http
|
|
316
|
+
|
|
317
|
+
result = await client.fail_phase("wr-1", error_message="compilation failed")
|
|
318
|
+
assert result is not None
|
|
319
|
+
|
|
320
|
+
@pytest.mark.asyncio
|
|
321
|
+
async def test_fail_phase_network_error(self):
|
|
322
|
+
client = WorkflowRunsClient(api_key="key")
|
|
323
|
+
mock_http = AsyncMock()
|
|
324
|
+
mock_http.post = AsyncMock(side_effect=httpx.RequestError("timeout"))
|
|
325
|
+
client._client = mock_http
|
|
326
|
+
|
|
327
|
+
result = await client.fail_phase("wr-1", error_message="oops")
|
|
328
|
+
assert result is None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class TestCancelWorkflowRun:
|
|
332
|
+
"""Tests for cancel_workflow_run."""
|
|
333
|
+
|
|
334
|
+
@pytest.mark.asyncio
|
|
335
|
+
async def test_cancel_success(self):
|
|
336
|
+
client = WorkflowRunsClient(api_key="key")
|
|
337
|
+
mock_response = httpx.Response(
|
|
338
|
+
200,
|
|
339
|
+
json=_make_workflow_run_json(status="cancelled"),
|
|
340
|
+
)
|
|
341
|
+
mock_http = AsyncMock()
|
|
342
|
+
mock_http.delete = AsyncMock(return_value=mock_response)
|
|
343
|
+
client._client = mock_http
|
|
344
|
+
|
|
345
|
+
result = await client.cancel_workflow_run("wr-1")
|
|
346
|
+
assert result is not None
|
|
347
|
+
assert result.status == "cancelled"
|
|
348
|
+
|
|
349
|
+
@pytest.mark.asyncio
|
|
350
|
+
async def test_cancel_failure(self):
|
|
351
|
+
client = WorkflowRunsClient(api_key="key")
|
|
352
|
+
mock_response = httpx.Response(404, text="Not found")
|
|
353
|
+
mock_http = AsyncMock()
|
|
354
|
+
mock_http.delete = AsyncMock(return_value=mock_response)
|
|
355
|
+
client._client = mock_http
|
|
356
|
+
|
|
357
|
+
result = await client.cancel_workflow_run("wr-nonexistent")
|
|
358
|
+
assert result is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/ci/canal-integration.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{steerdev-1.0.57 → steerdev-1.0.59}/src/steerdev_agent/setup/templates/worktrunk.config.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|