feat: bind WAC approval urls to a named wait_for_approval step (#10317)

* feat: bind WAC approval urls to a named wait_for_approval step

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: reject duplicate WAC approval step keys instead of renaming them

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: reject WAC approval links minted for a step that is not awaiting approval

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: bind WAC approval links to the awaiting step and stop step key aliasing

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: reject empty approval keys and scope minted-key writes to the workspace

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: enforce WAC approval binding at consumption and reject colliding keys

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: make WAC approval binding and collision checks atomic, harden TS step keys

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: decrement WAC suspend atomically instead of from a pre-lock snapshot

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: add sqlx cache entry for the atomic WAC suspend decrement

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: omit empty approver param from python get_approval_urls

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: pin the suspend-snapshot decrement and the colliding-mint race

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: drop the suspend-snapshot interleave test, it cannot both be stable and discriminate

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: reject step keys that cannot be minted as a URL path segment

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-07-25 11:41:48 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 15a6382c89
commit 9cef724ff2
29 changed files with 1676 additions and 228 deletions
+56 -1
View File
@@ -3,7 +3,7 @@
import asyncio
import pytest
from wmill.client import WorkflowCtx, _StepSuspend, TaskError, workflow, task, step, sleep, parallel, _run_workflow
from wmill.client import WorkflowCtx, _StepSuspend, TaskError, workflow, task, step, sleep, parallel, wait_for_approval, _run_workflow
@task
@@ -1112,3 +1112,58 @@ class TestParallel:
r = _run_workflow(wf, {}, {})
assert r["type"] == "complete"
assert r["result"] == []
class TestApprovalKeys:
"""`key` names the step that get_approval_urls() mints URLs against, so a
duplicate must fail rather than silently become `<key>_2` and leave the
caller holding a URL for the earlier step."""
def test_explicit_key_is_used_verbatim(self):
@workflow
async def wf():
return await wait_for_approval(key="manager")
assert _run_workflow(wf, {}, {})["key"] == "manager"
def test_duplicate_explicit_key_raises(self):
@workflow
async def wf():
await wait_for_approval(key="manager")
await wait_for_approval(key="manager")
with pytest.raises(RuntimeError, match="already used"):
_run_workflow(wf, {"completed_steps": {"manager": {"approved": True}}}, {})
def test_explicit_key_colliding_with_a_suffixed_step_key_raises(self):
"""`step("dup")` twice yields `dup`/`dup_2`, so an approval explicitly named
`dup_2` would alias the second step's key."""
@workflow
async def wf():
await step("dup", lambda: 1)
await step("dup", lambda: 2)
await wait_for_approval(key="dup_2")
with pytest.raises(RuntimeError, match="already used"):
_run_workflow(wf, {"completed_steps": {"dup": 1, "dup_2": 2}}, {})
@pytest.mark.parametrize("bad", ["", " ", ".", "..", "a/b"])
def test_unusable_key_raises(self, bad):
"""The key travels as one path segment when its URLs are minted, so anything
`get_approval_urls` could not address must be refused here too."""
@workflow
async def wf():
await wait_for_approval(key=bad)
with pytest.raises(RuntimeError, match="non-empty step name"):
_run_workflow(wf, {}, {})
def test_unnamed_approvals_still_auto_number(self):
@workflow
async def wf():
await wait_for_approval()
await wait_for_approval()
assert _run_workflow(wf, {"completed_steps": {"approval": {}}}, {})["key"] == "approval_2"