Skip to content
Closed
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
92 changes: 91 additions & 1 deletion swe_af/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Exposes:
- ``build``: end-to-end plan → execute → verify (single entry point)
- ``fast_build``: speed-optimised build with minimal review/retry cycles
- ``plan``: orchestrates product_manager → architect ↔ tech_lead → sprint_planner
- ``execute``: runs a planned DAG with self-healing replanning
"""
Expand Down Expand Up @@ -61,10 +62,99 @@ async def build(
If ``repo_url`` is provided and ``repo_path`` is empty, the repo is cloned
into ``/workspaces/<repo-name>`` automatically (useful in Docker).
"""
from swe_af.execution.schemas import BuildConfig, BuildResult
from swe_af.execution.schemas import BuildConfig

cfg = BuildConfig(**config) if config else BuildConfig()

return await _run_build(
goal=goal,
repo_path=repo_path,
artifacts_dir=artifacts_dir,
additional_context=additional_context,
cfg=cfg,
execute_fn_target=execute_fn_target,
max_turns=max_turns,
permission_mode=permission_mode,
enable_learning=enable_learning,
repo_url=repo_url,
)


@app.reasoner()
async def fast_build(
goal: str,
repo_path: str = "",
repo_url: str = "",
artifacts_dir: str = ".artifacts",
additional_context: str = "",
config: dict | None = None,
execute_fn_target: str = "",
max_turns: int = 0,
permission_mode: str = "",
enable_learning: bool = False,
) -> dict:
"""Speed-optimised build: skips review loops, replanning, and advisory steps.

Uses the same pipeline as ``build`` but with defaults tuned for maximum
throughput. Any key in *config* overrides these fast defaults.
"""
from swe_af.execution.schemas import BuildConfig

fast_defaults: dict = {
"max_review_iterations": 0,
"max_retries_per_issue": 0,
"max_replans": 0,
"enable_replanning": False,
"max_verify_fix_cycles": 0,
"max_coding_iterations": 1,
"max_advisor_invocations": 0,
"enable_issue_advisor": False,
"enable_integration_testing": False,
"agent_max_turns": 50,
"agent_timeout_seconds": 900,
"git_init_max_retries": 1,
}
merged = {**fast_defaults, **(config or {})}
cfg = BuildConfig(**merged)

return await _run_build(
goal=goal,
repo_path=repo_path,
artifacts_dir=artifacts_dir,
additional_context=additional_context,
cfg=cfg,
execute_fn_target=execute_fn_target,
max_turns=max_turns,
permission_mode=permission_mode,
enable_learning=enable_learning,
repo_url=repo_url,
)


# Expose the original functions via __wrapped__ so that inspect.getsource()
# can traverse the agentfield decorator and return the actual function source.
build.__wrapped__ = build._original_func # type: ignore[attr-defined]
fast_build.__wrapped__ = fast_build._original_func # type: ignore[attr-defined]


async def _run_build(
goal: str,
repo_path: str,
artifacts_dir: str,
additional_context: str,
cfg,
execute_fn_target: str = "",
max_turns: int = 0,
permission_mode: str = "",
enable_learning: bool = False,
repo_url: str = "",
) -> dict:
"""Internal async helper containing the full build pipeline body.

Called by both ``build`` and ``fast_build`` reasoners.
"""
from swe_af.execution.schemas import BuildResult

# Allow repo_url from config or direct parameter
if repo_url:
cfg.repo_url = repo_url
Expand Down
253 changes: 253 additions & 0 deletions tests/test_fast_build_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
"""Smoke tests for fast_build reasoner and related refactoring.

Verifies acceptance criteria for the fast_build feature:
- AC-1: fast_build is registered as a reasoner
- AC-2: fast_build and build have identical signatures
- AC-3: _run_build is async and not a registered reasoner
- AC-4: build is still registered as a reasoner
- AC-5: fast_build source contains all 12 fast default keys/values
- AC-6: fast_build applies fast defaults when no caller config is supplied
- AC-7: caller-supplied config overrides fast defaults
- AC-8: build() uses unmodified BuildConfig defaults
- AC-10: swe_af.app imports cleanly
"""

from __future__ import annotations

import asyncio
import inspect
import unittest
import unittest.mock as mock

from swe_af.execution.schemas import BuildConfig, BuildResult


def _make_fake_run_build(captured: dict):
"""Return a fake _run_build coroutine that captures the cfg dict."""

async def fake_run_build(goal, repo_path, artifacts_dir, additional_context, cfg, **kwargs):
captured.update(cfg.model_dump())
return BuildResult(
plan_result={},
dag_state={},
verification=None,
success=False,
summary="",
pr_url="",
).model_dump()

return fake_run_build


class TestFastBuildSmoke(unittest.TestCase):

# -----------------------------------------------------------------------
# AC-10: import sanity (run first so other tests can import cleanly)
# -----------------------------------------------------------------------

def test_ac10_module_imports_cleanly(self):
"""AC-10: swe_af.app imports without errors."""
import swe_af.app # noqa: F401

# -----------------------------------------------------------------------
# AC-1: fast_build registered as a reasoner
# -----------------------------------------------------------------------

def test_ac1_fast_build_registered_as_reasoner(self):
"""AC-1: fast_build is present in app.reasoners."""
from swe_af.app import app

# app.reasoners returns List[Dict] with 'id' keys
registered_ids = {r["id"] for r in app.reasoners}
self.assertIn(
"fast_build",
registered_ids,
f"fast_build not found in registered reasoners: {sorted(registered_ids)}",
)

# -----------------------------------------------------------------------
# AC-2: fast_build signature identical to build
# -----------------------------------------------------------------------

def test_ac2_signatures_identical(self):
"""AC-2: build and fast_build have the same parameter signatures."""
from swe_af.app import build, fast_build

build_sig = inspect.signature(build)
fast_sig = inspect.signature(fast_build)

build_params = {
k: (v.default, v.annotation)
for k, v in build_sig.parameters.items()
}
fast_params = {
k: (v.default, v.annotation)
for k, v in fast_sig.parameters.items()
}

self.assertEqual(
build_params,
fast_params,
f"Signatures differ:\nbuild: {build_params}\nfast_build: {fast_params}",
)

# -----------------------------------------------------------------------
# AC-3: _run_build is async and NOT a registered reasoner
# -----------------------------------------------------------------------

def test_ac3_run_build_is_async_and_not_a_reasoner(self):
"""AC-3: _run_build exists, is a coroutine function, is not a reasoner."""
from swe_af.app import _run_build, app

self.assertTrue(
inspect.iscoroutinefunction(_run_build),
"_run_build must be an async function",
)

registered_ids = {r["id"] for r in app.reasoners}
self.assertNotIn(
"_run_build",
registered_ids,
"_run_build must NOT be registered as a reasoner",
)

# -----------------------------------------------------------------------
# AC-4: build is still registered (no regression)
# -----------------------------------------------------------------------

def test_ac4_build_still_registered(self):
"""AC-4: build is still present in app.reasoners after refactoring."""
from swe_af.app import app

registered_ids = {r["id"] for r in app.reasoners}
self.assertIn(
"build",
registered_ids,
f"build not found in registered reasoners: {sorted(registered_ids)}",
)

# -----------------------------------------------------------------------
# AC-5: fast_build source contains all 12 fast default keys+values
# -----------------------------------------------------------------------

def test_ac5_fast_defaults_present_in_source(self):
"""AC-5: All 12 fast default keys and values appear in fast_build source."""
from swe_af import app as app_mod

src = inspect.getsource(app_mod.fast_build)

fast_defaults = {
"max_review_iterations": "0",
"max_retries_per_issue": "0",
"max_replans": "0",
"enable_replanning": "False",
"max_verify_fix_cycles": "0",
"max_coding_iterations": "1",
"max_advisor_invocations": "0",
"enable_issue_advisor": "False",
"enable_integration_testing": "False",
"agent_max_turns": "50",
"agent_timeout_seconds": "900",
"git_init_max_retries": "1",
}

for key, val in fast_defaults.items():
self.assertIn(key, src, f"Key not found in fast_build source: {key!r}")
self.assertIn(val, src, f"Value {val!r} not found in fast_build source (near {key!r})")

# -----------------------------------------------------------------------
# AC-6: fast_build with no config applies all fast defaults to BuildConfig
# -----------------------------------------------------------------------

def test_ac6_fast_defaults_applied_when_no_config(self):
"""AC-6: fast_build passes BuildConfig with all 12 fast defaults to _run_build."""
import swe_af.app as mod

captured: dict = {}
fake = _make_fake_run_build(captured)

with mock.patch.object(mod, "_run_build", fake):
asyncio.run(mod.fast_build(goal="test", repo_path="/tmp/fake"))

expects = {
"max_review_iterations": 0,
"max_retries_per_issue": 0,
"max_replans": 0,
"enable_replanning": False,
"max_verify_fix_cycles": 0,
"max_coding_iterations": 1,
"max_advisor_invocations": 0,
"enable_issue_advisor": False,
"enable_integration_testing": False,
"agent_max_turns": 50,
"agent_timeout_seconds": 900,
"git_init_max_retries": 1,
}

for key, expected_val in expects.items():
self.assertEqual(
captured[key],
expected_val,
f"{key}: expected {expected_val!r}, got {captured[key]!r}",
)

# -----------------------------------------------------------------------
# AC-7: caller-supplied config keys override fast defaults
# -----------------------------------------------------------------------

def test_ac7_caller_config_overrides_fast_defaults(self):
"""AC-7: A caller-supplied config key overrides the fast default for that key."""
import swe_af.app as mod

captured: dict = {}
fake = _make_fake_run_build(captured)

with mock.patch.object(mod, "_run_build", fake):
asyncio.run(
mod.fast_build(
goal="test",
repo_path="/tmp/fake",
config={"max_coding_iterations": 3},
)
)

# Caller override should take effect
self.assertEqual(
captured["max_coding_iterations"],
3,
f"Caller override ignored: expected 3, got {captured['max_coding_iterations']}",
)
# Other fast defaults should remain unchanged
self.assertEqual(
captured["max_review_iterations"],
0,
f"Unexpected change to max_review_iterations: {captured['max_review_iterations']}",
)

# -----------------------------------------------------------------------
# AC-8: build() passes unmodified BuildConfig defaults to _run_build
# -----------------------------------------------------------------------

def test_ac8_build_uses_unmodified_buildconfig_defaults(self):
"""AC-8: build() with no config passes BuildConfig with library-default values."""
import swe_af.app as mod

captured: dict = {}
fake = _make_fake_run_build(captured)

with mock.patch.object(mod, "_run_build", fake):
asyncio.run(mod.build(goal="test", repo_path="/tmp/fake"))

defaults = BuildConfig()

for field in ["max_review_iterations", "max_coding_iterations", "agent_max_turns"]:
default_val = getattr(defaults, field)
self.assertEqual(
captured[field],
default_val,
f"build() modified {field}: expected {default_val!r}, got {captured[field]!r}",
)


if __name__ == "__main__":
unittest.main()
Loading