diff --git a/task_flow_engine/engine.py b/task_flow_engine/engine.py index d005578..6b54671 100644 --- a/task_flow_engine/engine.py +++ b/task_flow_engine/engine.py @@ -69,7 +69,13 @@ class FlowEngine: ) ] 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 @staticmethod diff --git a/tests/test_routers_core.py b/tests/test_routers_core.py index 3750291..b02ab93 100644 --- a/tests/test_routers_core.py +++ b/tests/test_routers_core.py @@ -370,3 +370,25 @@ class TestFlowEndpoints: r = await client.get(f"/workstreams/{ws['id']}") 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" diff --git a/tests/test_task_flow_engine.py b/tests/test_task_flow_engine.py index f6805c6..a56b3b7 100644 --- a/tests/test_task_flow_engine.py +++ b/tests/test_task_flow_engine.py @@ -64,6 +64,50 @@ def test_failing_exit_assertion_identifies_blocking_assertion(): 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(): calls = [] diff --git a/workplans/STATE-WP-0047-lifecycle-assertions-and-renormalization.md b/workplans/STATE-WP-0047-lifecycle-assertions-and-renormalization.md index 72a1cd0..9761e2c 100644 --- a/workplans/STATE-WP-0047-lifecycle-assertions-and-renormalization.md +++ b/workplans/STATE-WP-0047-lifecycle-assertions-and-renormalization.md @@ -108,7 +108,7 @@ parents blocked. ```task id: STATE-WP-0047-T04 -status: todo +status: done priority: high 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 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 ```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 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 - Starting task work deterministically activates the parent workstream.