Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
"""Honesty of the manual-run delivery notice (issue #83993).
|
||||
|
||||
A manual ``cronjob(action='run')`` finishes with a completion summary line
|
||||
|
||||
Delivery target: <target> (output was delivered there by the job itself)
|
||||
|
||||
that was appended UNCONDITIONALLY for non-local targets — even when
|
||||
``run_one_job`` had just written ``last_delivery_error`` onto the refreshed
|
||||
job record because the post-run delivery (telegram/discord/…) failed. The
|
||||
calling agent then relayed "all good" over a failed delivery.
|
||||
|
||||
The note must follow the refreshed job record: a set ``last_delivery_error``
|
||||
means delivery FAILED with the error text surfaced; an empty/missing error
|
||||
keeps the legacy wording byte-for-byte (zero regression), and local jobs
|
||||
always say saved-locally.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.cronjob_tools import _manual_run_delivery_note
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_state():
|
||||
"""Reset the shared async-delegation world around each test.
|
||||
|
||||
The dispatch tests below submit real workers onto the process-wide
|
||||
daemon executor in ``tools.async_delegation``. A finished worker parks
|
||||
idle holding an ``_idle_semaphore`` token, so the NEXT dispatch in this
|
||||
process REUSES that thread instead of spawning a fresh one — and only
|
||||
the fresh-spawn path keeps upstream's dispatch-and-return test winning
|
||||
its patch-visibility race: ``Thread.start()`` blocks the dispatching
|
||||
thread until the worker has bootstrapped, so the worker performs
|
||||
``_run_claimed_job``'s lazy ``from cron.scheduler import run_one_job``
|
||||
while the test's patches are still active. On the idle-reuse path
|
||||
``submit`` returns with the GIL still held, the patch block unwinds
|
||||
first, and the worker binds the REAL ``run_one_job`` — which then runs
|
||||
the fake job for real ("no model configured") and the mock never fires.
|
||||
Without this reset, test_cronjob_run_background.py's
|
||||
``test_dispatches_and_returns_handle_immediately`` fails
|
||||
deterministically whenever this file runs before it. Mirrors
|
||||
``tests/tools/test_async_delegation.py::_clean_state``.
|
||||
"""
|
||||
from tools import async_delegation as ad
|
||||
from tools.process_registry import process_registry
|
||||
|
||||
ad._reset_for_tests()
|
||||
while not process_registry.completion_queue.empty():
|
||||
process_registry.completion_queue.get_nowait()
|
||||
yield
|
||||
# Give just-drained workers a beat to finalize BEFORE resetting, so
|
||||
# their completion events land now instead of leaking into the next
|
||||
# test's queue (mirrors test_async_delegation.py).
|
||||
deadline = time.monotonic() + 2.0
|
||||
while ad.active_count() and time.monotonic() < deadline:
|
||||
time.sleep(0.02)
|
||||
ad._reset_for_tests()
|
||||
while not process_registry.completion_queue.empty():
|
||||
process_registry.completion_queue.get_nowait()
|
||||
|
||||
|
||||
def _job(job_id, deliver):
|
||||
"""Per-test job dict with a UNIQUE id.
|
||||
|
||||
Background workers outlive their test (daemon executor) and hold the id
|
||||
in the scheduler's shared running set until the run finishes; reusing an
|
||||
id across tests trips the in-flight dedupe guard on a straggler.
|
||||
"""
|
||||
return {
|
||||
"id": job_id,
|
||||
"name": f"dn run {job_id}",
|
||||
"prompt": "hi",
|
||||
"schedule": {"kind": "cron", "expr": "0 9 * * *"},
|
||||
"deliver": deliver,
|
||||
}
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _bound_session_key(key):
|
||||
"""Bind the approval session key contextvar (background dispatch gate)."""
|
||||
from tools.approval import _approval_session_key
|
||||
|
||||
token = _approval_session_key.set(key)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_approval_session_key.reset(token)
|
||||
|
||||
|
||||
def _dispatch_diag(res) -> str:
|
||||
"""Failure renderer for the wiring tests' dispatch asserts: the result
|
||||
dict plus the scheduler running set, so a broken assert names the return
|
||||
path that was actually taken instead of a bare KeyError."""
|
||||
try:
|
||||
from cron.scheduler import get_running_job_ids
|
||||
|
||||
running = sorted(get_running_job_ids())
|
||||
except Exception as e: # pragma: no cover - diagnostic only
|
||||
running = f"<unavailable: {e}>"
|
||||
return f"dispatch result: {res!r}; running: {running}"
|
||||
|
||||
|
||||
def _drain_completion_event(delegation_id):
|
||||
"""Wait (bounded) for this delegation's completion event; requeue others.
|
||||
|
||||
The runner executes on a daemon thread, so this must be called while the
|
||||
test's patches are still active.
|
||||
"""
|
||||
from tools.process_registry import process_registry
|
||||
|
||||
for _ in range(100):
|
||||
try:
|
||||
evt = process_registry.completion_queue.get_nowait()
|
||||
except Exception:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
if evt.get("delegation_id") == delegation_id:
|
||||
return evt
|
||||
process_registry.completion_queue.put(evt)
|
||||
time.sleep(0.05)
|
||||
return None
|
||||
|
||||
|
||||
class TestDeliveryNote:
|
||||
"""``_manual_run_delivery_note`` — the summary-line wording contract."""
|
||||
|
||||
def test_local_always_saved_locally_only(self):
|
||||
expected = " (output saved locally only)"
|
||||
assert _manual_run_delivery_note("local", {}) == expected
|
||||
# Local jobs never deliver — a stale delivery error must not leak in.
|
||||
assert (
|
||||
_manual_run_delivery_note("local", {"last_delivery_error": "telegram 400"})
|
||||
== expected
|
||||
)
|
||||
|
||||
def test_remote_without_error_keeps_legacy_wording(self):
|
||||
expected = " (output was delivered there by the job itself)"
|
||||
assert _manual_run_delivery_note("telegram", {}) == expected
|
||||
assert (
|
||||
_manual_run_delivery_note("telegram", {"last_delivery_error": None})
|
||||
== expected
|
||||
)
|
||||
assert (
|
||||
_manual_run_delivery_note("discord:#ops", {"last_delivery_error": " "})
|
||||
== expected
|
||||
)
|
||||
|
||||
def test_empty_or_missing_deliver_reads_saved_locally(self):
|
||||
"""Falsy deliver = no target, and the fire-time path treats it as
|
||||
"local" (no delivery, no delivery error) — the note must not claim
|
||||
"delivered there" for a target that doesn't exist (#83993 class)."""
|
||||
expected = " (output saved locally only)"
|
||||
assert _manual_run_delivery_note("", {}) == expected
|
||||
assert _manual_run_delivery_note(None, {}) == expected
|
||||
# Falsy deliver never attempts delivery — a stale error (e.g. from an
|
||||
# earlier deliver config) must not flip the wording either.
|
||||
assert _manual_run_delivery_note("", {"last_delivery_error": "old"}) == expected
|
||||
|
||||
def test_whitespace_deliver_defers_to_error_record(self):
|
||||
"""Whitespace-only deliver is NOT folded into local: fire time lets it
|
||||
through as a target that fails to resolve, so the recorded error must
|
||||
stay visible rather than being masked by a saved-locally wording."""
|
||||
note = _manual_run_delivery_note(" ", {"last_delivery_error": "no target"})
|
||||
assert "delivery FAILED" in note
|
||||
assert "no target" in note
|
||||
|
||||
def test_remote_with_error_says_delivery_failed(self):
|
||||
note = _manual_run_delivery_note(
|
||||
"telegram", {"last_delivery_error": "send failed: 400 Bad Request"}
|
||||
)
|
||||
assert "delivery FAILED" in note
|
||||
assert "send failed: 400 Bad Request" in note
|
||||
|
||||
def test_remote_error_text_truncated_to_200_chars(self):
|
||||
note = _manual_run_delivery_note("telegram", {"last_delivery_error": "E" * 500})
|
||||
assert "E" * 200 in note
|
||||
assert "E" * 201 not in note
|
||||
|
||||
|
||||
class TestRunnerSummaryWiring:
|
||||
"""The completion event the calling agent actually sees must follow the
|
||||
refreshed job record — both directions of issue #83993."""
|
||||
|
||||
def test_delivery_failure_surfaces_in_completion_summary(self):
|
||||
from tools.cronjob_tools import _try_dispatch_background_run
|
||||
|
||||
job = _job("job-dn-01", "telegram")
|
||||
with _bound_session_key("agent:main:telegram:dm:83993"):
|
||||
with (
|
||||
patch(
|
||||
"tools.cronjob_tools.claim_job_for_fire",
|
||||
return_value=job, # claimed snapshot (return_job=True API)
|
||||
),
|
||||
patch("cron.scheduler.run_one_job", return_value=True),
|
||||
patch(
|
||||
"tools.cronjob_tools.get_job",
|
||||
return_value={
|
||||
# Post-#83993 record shape: mark_job_run writes
|
||||
# delivery_failed (not ok) when only delivery failed.
|
||||
"last_status": "delivery_failed",
|
||||
"last_error": None,
|
||||
"last_delivery_error": "telegram send failed: 400",
|
||||
},
|
||||
),
|
||||
):
|
||||
res = _try_dispatch_background_run(job)
|
||||
assert res.get("dispatched") is True, _dispatch_diag(res)
|
||||
evt = _drain_completion_event(res["delegation_id"])
|
||||
assert evt is not None, "completion event never reached the queue"
|
||||
summary = evt.get("summary") or ""
|
||||
assert "Delivery target: telegram" in summary
|
||||
assert "delivery FAILED" in summary
|
||||
assert "telegram send failed: 400" in summary
|
||||
assert "delivered there by the job itself" not in summary
|
||||
# The headline must not read "Result: ok" over an undelivered run.
|
||||
assert "Result: FAILED" in summary
|
||||
assert "Result: ok" not in summary
|
||||
|
||||
def test_empty_deliver_summary_states_local_not_phantom_target(self):
|
||||
"""End-to-end: an empty stored deliver must render as the local target
|
||||
it behaves as at fire time — never a bare "Delivery target: " followed
|
||||
by a delivered-there claim."""
|
||||
from tools.cronjob_tools import _try_dispatch_background_run
|
||||
|
||||
job = _job("job-dn-03", "")
|
||||
with _bound_session_key("agent:main:telegram:dm:86622"):
|
||||
with (
|
||||
patch(
|
||||
"tools.cronjob_tools.claim_job_for_fire",
|
||||
return_value=job, # claimed snapshot (return_job=True API)
|
||||
),
|
||||
patch("cron.scheduler.run_one_job", return_value=True),
|
||||
patch(
|
||||
"tools.cronjob_tools.get_job",
|
||||
return_value={"last_status": "ok", "last_error": None},
|
||||
),
|
||||
):
|
||||
res = _try_dispatch_background_run(job)
|
||||
assert res.get("dispatched") is True, _dispatch_diag(res)
|
||||
evt = _drain_completion_event(res["delegation_id"])
|
||||
assert evt is not None, "completion event never reached the queue"
|
||||
summary = evt.get("summary") or ""
|
||||
assert "Delivery target: local (output saved locally only)" in summary
|
||||
assert "delivered there by the job itself" not in summary
|
||||
|
||||
def test_delivery_success_wording_unchanged_in_completion_summary(self):
|
||||
from tools.cronjob_tools import _try_dispatch_background_run
|
||||
|
||||
job = _job("job-dn-02", "telegram")
|
||||
with _bound_session_key("agent:main:telegram:dm:83994"):
|
||||
with (
|
||||
patch(
|
||||
"tools.cronjob_tools.claim_job_for_fire",
|
||||
return_value=job, # claimed snapshot (return_job=True API)
|
||||
),
|
||||
patch("cron.scheduler.run_one_job", return_value=True),
|
||||
patch(
|
||||
"tools.cronjob_tools.get_job",
|
||||
return_value={"last_status": "ok", "last_error": None},
|
||||
),
|
||||
):
|
||||
res = _try_dispatch_background_run(job)
|
||||
assert res.get("dispatched") is True, _dispatch_diag(res)
|
||||
evt = _drain_completion_event(res["delegation_id"])
|
||||
assert evt is not None, "completion event never reached the queue"
|
||||
summary = evt.get("summary") or ""
|
||||
assert (
|
||||
"Delivery target: telegram (output was delivered there by the job itself)"
|
||||
) in summary
|
||||
assert "delivery FAILED" not in summary
|
||||
Reference in New Issue
Block a user