项目文件夹

文件
2026-06-04 00:53:00 +00:00

217 行
7.6 KiB
Python

from __future__ import annotations
import asyncio
from pathlib import Path
from types import SimpleNamespace
import pytest
from openharness.commands.registry import CommandContext, create_default_command_registry
from openharness.tools.base import ToolExecutionContext
from openharness.tools.workflow_tool import WorkflowCreateInput, WorkflowCreateTool
from openharness.workflows.manager import get_workflow_manager, reset_workflow_manager
from openharness.workflows.runtime import WorkflowRuntime
from openharness.workflows.store import WorkflowStore
@pytest.fixture(autouse=True)
def _reset_workflow_manager():
reset_workflow_manager()
yield
reset_workflow_manager()
class EchoWorkflowAgentRunner:
def __init__(self, *, fail_if_called: bool = False) -> None:
self.calls: list[str] = []
self.running = 0
self.max_running = 0
self.fail_if_called = fail_if_called
async def run_agent(self, *, prompt: str, cwd: Path, model: str | None, phase: str | None, metadata: dict):
del cwd, model, metadata
if self.fail_if_called:
raise AssertionError("cache hit should avoid live agent execution")
self.calls.append(f"{phase or '-'}:{prompt}")
self.running += 1
self.max_running = max(self.max_running, self.running)
try:
await asyncio.sleep(0.03)
return f"OUT[{phase or '-'}]:{prompt}", f"task-{len(self.calls)}", None
finally:
self.running -= 1
WORKFLOW_SCRIPT = """
return await workflow("demo-workflow", async () => {
const items = ["a", "b", "c"];
const first = await phase("fanout", async () => {
return await parallel(items, item => agent(`inspect ${item}`, { phase: "fanout", name: `worker-${item}` }));
});
const reviewed = await phase("review", async () => {
return await pipeline(first, [
item => agent(`review ${item}`, { phase: "review" }),
item => agent(`finalize ${item}`, { phase: "review" }),
]);
});
await log(`reviewed ${reviewed.length} items`);
return reviewed.join("\\n");
});
"""
BARE_WORKFLOW_SCRIPT = """
workflow("bare-workflow", async () => {
const result = await phase("single", async () => {
return await agent("inspect bare workflow", { phase: "single", name: "worker" });
});
return `bare:${result}`;
});
"""
LABELED_AGENT_PHASE_SCRIPT = """
return workflow("labeled-agent-phase", async () => {
const results = await parallel(["a", "b"], item => {
return agent(`inspect ${item}`, { phase: "labeled-review" });
});
return results.join(",");
});
"""
@pytest.mark.asyncio
async def test_workflow_runtime_runs_parallel_pipeline_and_records_snapshot(tmp_path: Path):
store = WorkflowStore(tmp_path / "wf")
run = store.create_run(cwd=tmp_path, name="demo", script=WORKFLOW_SCRIPT, max_concurrency=4)
runner = EchoWorkflowAgentRunner()
result = await WorkflowRuntime(store=store, run=run, agent_runner=runner, model="test-model").run_script()
assert "finalize OUT[review]" in result
assert len(runner.calls) == 9
assert runner.max_running > 1
saved = store.load_run(run.id)
assert saved is not None
assert saved.status == "completed"
assert saved.agent_count == 9
assert saved.phases["fanout"].agent_count == 3
assert saved.phases["review"].agent_count == 6
assert "reviewed 3 items" in saved.logs
@pytest.mark.asyncio
async def test_workflow_runtime_resume_uses_cached_agent_results(tmp_path: Path):
store = WorkflowStore(tmp_path / "wf")
run = store.create_run(cwd=tmp_path, name="demo", script=WORKFLOW_SCRIPT, max_concurrency=4)
await WorkflowRuntime(
store=store,
run=run,
agent_runner=EchoWorkflowAgentRunner(),
model="test-model",
).run_script()
resumed = store.clone_run_for_resume(run.id)
assert resumed is not None
result = await WorkflowRuntime(
store=store,
run=resumed,
agent_runner=EchoWorkflowAgentRunner(fail_if_called=True),
model="test-model",
).run_script()
assert "finalize OUT[review]" in result
saved = store.load_run(run.id)
assert saved is not None
assert saved.status == "completed"
assert saved.cached_count >= 9
@pytest.mark.asyncio
async def test_workflow_runtime_waits_for_bare_workflow_call(tmp_path: Path):
store = WorkflowStore(tmp_path / "wf")
run = store.create_run(cwd=tmp_path, name="bare", script=BARE_WORKFLOW_SCRIPT, max_concurrency=2)
runner = EchoWorkflowAgentRunner()
result = await WorkflowRuntime(store=store, run=run, agent_runner=runner, model="test-model").run_script()
assert result == "bare:OUT[single]:inspect bare workflow"
assert runner.calls == ["single:inspect bare workflow"]
saved = store.load_run(run.id)
assert saved is not None
assert saved.status == "completed"
assert saved.agent_count == 1
assert saved.phases["single"].agent_count == 1
@pytest.mark.asyncio
async def test_workflow_runtime_completes_agent_labeled_phases(tmp_path: Path):
store = WorkflowStore(tmp_path / "wf")
run = store.create_run(cwd=tmp_path, name="labeled", script=LABELED_AGENT_PHASE_SCRIPT, max_concurrency=2)
await WorkflowRuntime(
store=store,
run=run,
agent_runner=EchoWorkflowAgentRunner(),
model="test-model",
).run_script()
saved = store.load_run(run.id)
assert saved is not None
assert saved.status == "completed"
assert saved.phases["labeled-review"].status == "completed"
assert saved.phases["labeled-review"].agent_count == 2
@pytest.mark.asyncio
async def test_workflow_create_tool_saves_and_runs_workflow(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
monkeypatch.setenv("OPENHARNESS_DATA_DIR", str(tmp_path / "data"))
reset_workflow_manager()
manager = get_workflow_manager()
manager.set_agent_runner(EchoWorkflowAgentRunner())
result = await WorkflowCreateTool().execute(
WorkflowCreateInput(
name="Demo Tool Workflow",
script=WORKFLOW_SCRIPT,
run_immediately=True,
wait_for_completion=True,
max_concurrency=4,
),
ToolExecutionContext(cwd=tmp_path, metadata={"model": "test-model"}),
)
assert result.is_error is False
assert "Saved workflow" in result.output
assert "completed" in result.output
assert result.metadata["run_id"]
assert (tmp_path / ".openharness" / "workflows" / "demo-tool-workflow.js").exists()
@pytest.mark.asyncio
async def test_workflows_slash_command_runs_and_shows_saved_workflow(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
monkeypatch.setenv("OPENHARNESS_DATA_DIR", str(tmp_path / "data"))
reset_workflow_manager()
manager = get_workflow_manager()
manager.set_agent_runner(EchoWorkflowAgentRunner())
manager.save_script(cwd=tmp_path, name="demo", script=WORKFLOW_SCRIPT)
registry = create_default_command_registry()
context = CommandContext(
engine=SimpleNamespace(model="test-model"), # type: ignore[arg-type]
cwd=str(tmp_path),
)
command, args = registry.lookup("/workflows run demo")
result = await command.handler(args, context)
assert "Started workflow" in result.message
run_id = result.message.split()[2].rstrip(":")
await manager.wait(run_id)
command, args = registry.lookup(f"/workflows output {run_id}")
output_result = await command.handler(args, context)
assert "completed" in output_result.message
assert "result:" in output_result.message