diff --git a/core/workflow_adapters.py b/core/workflow_adapters.py index ef7f255..ff58a2b 100644 --- a/core/workflow_adapters.py +++ b/core/workflow_adapters.py @@ -165,6 +165,18 @@ def applier(*, state: RunState, result: Mapping[str, Any], run: Any | None = Non client = _client_factory(state.installation_id, github_client_factory) repo_handle = client.get_repo(state.repo) progress = workflow.progress_for_state(repo_handle, state=state) + # Record the Oz session link onto the progress comment before + # applying the result. A run that reaches a terminal SUCCEEDED + # state on the first cron poll never passes through + # ``non_terminal_handler``, so without this the completion comment + # posted back to the PR/issue -- including the "completed with no + # work" message -- would omit the link back to the Warp + # conversation that explains what the agent did. ``run_adapter`` + # derives its ``session_link`` from the progress comment, so this + # must run before the adapter is built. Mirrors the failure + # handler, which records the link before ``report_error``. + if run is not None: + record_session_link_safely(progress, run) run_adapter = workflow.run_adapter_for_state(state=state, progress=progress, run=run) try: workflow.apply_result( diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 979b50b..9be6f0d 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -296,6 +296,57 @@ def test_result_applier_invokes_apply_pr_comment_result(self) -> None: # posting onto the PR conversation. repo_handle.get_pull.assert_called_once_with(7) + def test_result_applier_records_session_link_for_terminal_run(self) -> None: + # A run that is already terminal on the first cron poll never + # passes through ``non_terminal_handler``. The applier must still + # record the session link so the completion comment posted back + # to the PR (including the "completed with no work" message) + # links to the Warp conversation that explains what the agent did. + from core.handlers import build_respond_handlers + + github_client = MagicMock() + github_client.get_repo.return_value = MagicMock(name="repo") + handlers = build_respond_handlers(_factory(github_client)) + + state = _state( + "respond-to-pr-comment", + payload_subset={ + "owner": "acme", + "repo": "widgets", + "pr_number": 7, + "head_branch": "feature", + "trigger_kind": "conversation", + "requester": "alice", + "progress_comment_id": 6543, + }, + ) + run = MagicMock( + state="SUCCEEDED", + session_link="https://app.warp.dev/run/abc", + run_id="oz-run-123", + ) + handlers.result_applier(state=state, result={}, run=run) + + helpers = sys.modules["oz.helpers"] + helpers.record_run_session_link.assert_called_once_with( # type: ignore[attr-defined] + self.progress_instances[-1], run + ) + + def test_result_applier_skips_session_link_when_run_missing(self) -> None: + # Synchronous callers and tests may invoke the applier without a + # run handle; recording must be skipped rather than passing + # ``None`` into ``record_run_session_link``. + from core.handlers import build_respond_handlers + + github_client = MagicMock() + github_client.get_repo.return_value = MagicMock(name="repo") + handlers = build_respond_handlers(_factory(github_client)) + + handlers.result_applier(state=_state("respond-to-pr-comment"), result={}) + + helpers = sys.modules["oz.helpers"] + helpers.record_run_session_link.assert_not_called() # type: ignore[attr-defined] + class VerifyHandlersTest(_HandlerTestBase): def test_artifact_loader_calls_load_run_artifact_with_report_filename(self) -> None: