Fix exact gateway approval producer resolution
This commit is contained in:
1 parent
571ef3eec2
commit
8c92d88225
4 files changed
+147
-50
No files matched your search
@@ -601,6 +601,85 @@ class TestApprovalHTTPEndpoints:
|
||||
r._gateway_queues.pop(sid, None)
|
||||
_STREAM_RUN_IDS.pop(stream_id, None)
|
||||
|
||||
def test_identity_v1_non_head_run_approval_settles_exact_producer(self, monkeypatch):
|
||||
"""A relayed non-head run approval must wake only its own parked waiter."""
|
||||
from api import route_approvals as ra
|
||||
from api import routes as r
|
||||
|
||||
sid = f"identity-v1-non-head-{uuid.uuid4().hex[:8]}"
|
||||
run_id = f"run-identity-v1-{uuid.uuid4().hex[:8]}"
|
||||
head = {
|
||||
"approval_id": "head-approval",
|
||||
"run_id": run_id,
|
||||
"command": "head command",
|
||||
"_gateway_agent_identity_v1": True,
|
||||
}
|
||||
target = {
|
||||
"approval_id": "target-approval",
|
||||
"run_id": run_id,
|
||||
"command": "target command",
|
||||
"_gateway_agent_identity_v1": True,
|
||||
}
|
||||
head_entry = _ApprovalEntry(dict(head))
|
||||
target_entry = _ApprovalEntry(dict(target))
|
||||
captured = {}
|
||||
relays = []
|
||||
|
||||
def fake_j(_handler, data, status=200, extra_headers=None):
|
||||
captured.update(payload=data, status=status)
|
||||
return data
|
||||
|
||||
def fake_respond(_self, got_run_id, got_approval_id, choice):
|
||||
relays.append((got_run_id, got_approval_id, choice))
|
||||
return {"resolved": 1}
|
||||
|
||||
monkeypatch.setattr(r, "j", fake_j)
|
||||
monkeypatch.setattr("api.runner_client.HttpRunnerClient.respond_approval", fake_respond)
|
||||
monkeypatch.setattr(
|
||||
"api.config.gateway_supports_approval_identity_v1", lambda *_args: True
|
||||
)
|
||||
with _lock:
|
||||
r._pending.pop(sid, None)
|
||||
r._gateway_queues[sid] = [head_entry, target_entry]
|
||||
try:
|
||||
ra.submit_gateway_pending_mirror(sid, dict(target_entry.data))
|
||||
mirror = ra.gateway_pending_mirror(
|
||||
sid, approval_id=target["approval_id"], run_id=run_id
|
||||
)
|
||||
assert mirror is not None
|
||||
|
||||
r._handle_approval_respond(
|
||||
object(),
|
||||
{
|
||||
"session_id": sid,
|
||||
"choice": "deny",
|
||||
"approval_id": target["approval_id"],
|
||||
"run_id": run_id,
|
||||
"mirror_token": mirror[ra._GATEWAY_MIRROR_TOKEN],
|
||||
},
|
||||
)
|
||||
|
||||
assert captured == {
|
||||
"payload": {"ok": True, "choice": "deny", "relayed": True},
|
||||
"status": 200,
|
||||
}
|
||||
assert relays == [(run_id, target["approval_id"], "deny")]
|
||||
assert target_entry.event.is_set()
|
||||
assert target_entry.result == "deny"
|
||||
assert not head_entry.event.is_set()
|
||||
with _lock:
|
||||
assert r._gateway_queues[sid] == [head_entry]
|
||||
for _ in range(2):
|
||||
with _lock:
|
||||
ra.reconcile_gateway_pending_mirror_locked(sid)
|
||||
assert ra.gateway_pending_mirror(
|
||||
sid, approval_id=target["approval_id"], run_id=run_id
|
||||
) is None
|
||||
finally:
|
||||
with _lock:
|
||||
r._pending.pop(sid, None)
|
||||
r._gateway_queues.pop(sid, None)
|
||||
|
||||
def test_gateway_mirror_without_run_id_with_one_producer_resolves_exactly(self, monkeypatch):
|
||||
"""A no-run mirror retires only after its exact local producer resolves."""
|
||||
from api import routes as r
|
||||
|
||||
@@ -875,13 +875,13 @@ def test_legacy_approval_without_run_id_retires_locally():
|
||||
_STREAM_RUN_IDS.pop(stream_id, None)
|
||||
|
||||
|
||||
def test_deny_retires_run_producer_so_reconciliation_cannot_resurrect_mirror():
|
||||
"""Deny must consume the producer as well as its WebUI mirror.
|
||||
def test_deny_settles_run_producer_so_reconciliation_cannot_resurrect_mirror():
|
||||
"""Deny must settle the producer as well as its WebUI mirror.
|
||||
|
||||
Selection is the behaviour under test, so the queue carries more than the
|
||||
one entry being retired: a sibling approval for the same run, and a producer
|
||||
belonging to a different run. Retiring one approval must consume exactly
|
||||
that producer and leave both others queued.
|
||||
one entry being resolved: a sibling approval for the same run, and a producer
|
||||
belonging to a different run. Resolving one approval must consume exactly
|
||||
that producer, wake its waiter, and leave both others queued.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
from api import route_approvals as ra
|
||||
@@ -925,12 +925,13 @@ def test_deny_retires_run_producer_so_reconciliation_cannot_resurrect_mirror():
|
||||
mirror = ra.gateway_pending_mirror(sid, approval_id=approval_id, run_id=run_id)
|
||||
assert mirror is not None
|
||||
|
||||
assert ra.retire_gateway_pending_mirror(
|
||||
resolved, _head, _total = ra.resolve_gateway_pending_run(
|
||||
sid,
|
||||
approval_id=approval_id,
|
||||
run_id=run_id,
|
||||
mirror_token=mirror[ra._GATEWAY_MIRROR_TOKEN],
|
||||
choice="deny",
|
||||
)
|
||||
assert resolved == 1
|
||||
|
||||
assert queued_before == [target, same_run_sibling, other_run], (
|
||||
"the queue should have held all three producers before retirement"
|
||||
@@ -938,6 +939,8 @@ def test_deny_retires_run_producer_so_reconciliation_cannot_resurrect_mirror():
|
||||
with ra._lock:
|
||||
remaining = list(ra._gateway_queues.get(sid) or [])
|
||||
assert target not in remaining, "the denied approval's producer must be consumed"
|
||||
assert target.event.is_set(), "the denied approval's waiter must be woken"
|
||||
assert target.result == "deny"
|
||||
assert same_run_sibling in remaining, (
|
||||
"a sibling approval on the same run must not be consumed by retiring another approval"
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user