generated from coulomb/repo-seed
Harden flow advancement exit assertions
This commit is contained in:
@@ -69,7 +69,13 @@ class FlowEngine:
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
evaluator = AssertionEvaluator(custom_ops=self.custom_ops)
|
evaluator = AssertionEvaluator(custom_ops=self.custom_ops)
|
||||||
failed = self._failed_assertions(workstation.entry_assertions, obj, evaluator)
|
current_name = str(obj.get("workstation") or obj.get("status") or "")
|
||||||
|
failed: list[AssertionResult] = []
|
||||||
|
if target_workstation != current_name:
|
||||||
|
current = flow.workstation(current_name)
|
||||||
|
if current is not None:
|
||||||
|
failed.extend(self._failed_assertions(current.exit_assertions, obj, evaluator))
|
||||||
|
failed.extend(self._failed_assertions(workstation.entry_assertions, obj, evaluator))
|
||||||
return not failed, failed
|
return not failed, failed
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -370,3 +370,25 @@ class TestFlowEndpoints:
|
|||||||
|
|
||||||
r = await client.get(f"/workstreams/{ws['id']}")
|
r = await client.get(f"/workstreams/{ws['id']}")
|
||||||
assert r.json()["status"] == "finished"
|
assert r.json()["status"] == "finished"
|
||||||
|
|
||||||
|
async def test_advance_workstream_respects_current_exit_assertions(self, client):
|
||||||
|
await _create_domain(client)
|
||||||
|
topic = await _create_topic(client)
|
||||||
|
ws = await _create_workstream(client, topic["id"], slug="exit-blocked-ws")
|
||||||
|
dependency_ws = await _create_workstream(client, topic["id"], slug="unfinished-dep")
|
||||||
|
task = await _create_task(client, ws["id"])
|
||||||
|
await client.patch(f"/tasks/{task['id']}", json={"status": "done"})
|
||||||
|
await client.post(
|
||||||
|
f"/workstreams/{ws['id']}/dependencies/",
|
||||||
|
json={
|
||||||
|
"to_workstream_id": dependency_ws["id"],
|
||||||
|
"description": "Dependency must finish first",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
r = await client.post(f"/flows/workstream/{ws['id']}/advance/finished")
|
||||||
|
assert r.status_code == 409
|
||||||
|
assert r.json()["detail"]["blocking_assertions"][0]["id"] == "dependencies.all_complete"
|
||||||
|
|
||||||
|
r = await client.get(f"/workstreams/{ws['id']}")
|
||||||
|
assert r.json()["status"] == "active"
|
||||||
|
|||||||
@@ -64,6 +64,50 @@ def test_failing_exit_assertion_identifies_blocking_assertion():
|
|||||||
assert result.blocking_assertions[0].actual == ["done", "todo"]
|
assert result.blocking_assertions[0].actual == ["done", "todo"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_reach_checks_current_exit_assertions_before_target_entry():
|
||||||
|
flow = FlowDef(
|
||||||
|
id="workstream.test",
|
||||||
|
entity_type="workstream",
|
||||||
|
workstations=[
|
||||||
|
WorkstationDef(
|
||||||
|
name="active",
|
||||||
|
exit_assertions=[
|
||||||
|
AssertionDef(
|
||||||
|
id="dependencies.all_complete",
|
||||||
|
target="dependencies.*.workstation",
|
||||||
|
op="all_eq",
|
||||||
|
value=["finished", "archived"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
WorkstationDef(
|
||||||
|
name="finished",
|
||||||
|
entry_assertions=[
|
||||||
|
AssertionDef(
|
||||||
|
id="tasks.all_done",
|
||||||
|
target="tasks.*.status",
|
||||||
|
op="all_eq",
|
||||||
|
value=["done", "cancelled"],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
can_reach, failures = FlowEngine().can_reach(
|
||||||
|
{
|
||||||
|
"status": "active",
|
||||||
|
"tasks": [{"status": "done"}],
|
||||||
|
"dependencies": [{"workstation": "active"}],
|
||||||
|
},
|
||||||
|
flow,
|
||||||
|
"finished",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert can_reach is False
|
||||||
|
assert [item.id for item in failures] == ["dependencies.all_complete"]
|
||||||
|
|
||||||
|
|
||||||
def test_custom_op_callable_is_invoked():
|
def test_custom_op_callable_is_invoked():
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ parents blocked.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: STATE-WP-0047-T04
|
id: STATE-WP-0047-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "3f1e49fd-0600-4124-a7bc-0c75955bac8b"
|
state_hub_task_id: "3f1e49fd-0600-4124-a7bc-0c75955bac8b"
|
||||||
```
|
```
|
||||||
@@ -119,6 +119,11 @@ assertions. Return actionable blocking assertions when a transition is refused.
|
|||||||
Done when `/flows/.../advance/...` cannot bypass the same assertions the flow
|
Done when `/flows/.../advance/...` cannot bypass the same assertions the flow
|
||||||
state endpoint reports as blocking.
|
state endpoint reports as blocking.
|
||||||
|
|
||||||
|
Result 2026-05-23: `FlowEngine.can_reach()` now checks current exit assertions
|
||||||
|
before target entry assertions for real transitions. The flow advance endpoint
|
||||||
|
returns HTTP 409 when a dependency blocks leaving `active`, even if the target
|
||||||
|
state entry assertions pass.
|
||||||
|
|
||||||
## T05 - Add Renormalization Checks And Repairs
|
## T05 - Add Renormalization Checks And Repairs
|
||||||
|
|
||||||
```task
|
```task
|
||||||
@@ -169,6 +174,9 @@ from `proposed`, `ready`, and `backlog`, plus a guard test proving `blocked`
|
|||||||
parents stay blocked. Remaining coverage still needs flow assertion hardening
|
parents stay blocked. Remaining coverage still needs flow assertion hardening
|
||||||
and consistency repair tests.
|
and consistency repair tests.
|
||||||
|
|
||||||
|
Progress 2026-05-23: added engine and router coverage proving flow advancement
|
||||||
|
honors current exit assertions before moving to the target workstation.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
- Starting task work deterministically activates the parent workstream.
|
- Starting task work deterministically activates the parent workstream.
|
||||||
|
|||||||
Reference in New Issue
Block a user