From 0443b734c14abaaad0cc2096f1e25382dbd4ebce Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 26 Jul 2026 16:15:32 +0200 Subject: [PATCH] fix(tests): pytest nach Kernel Spine und Starter-Kit bereinigen list_compatible_methods nur Primary-Methoden; Agile-Graph auf Sprint-Scope; Tests ohne Starter-Action-Kollision; TenantContext-Factory fuer Kernel-Tests. Co-authored-by: Cursor --- backend/steering/methods/registry.py | 3 +- .../strategies/next_action/agile_iteration.py | 20 ++++++-- backend/tests/factories.py | 48 +++++++++++++++++++ .../tests/test_ap22b_linear_critical_path.py | 1 + .../tests/test_ap22c_sequential_horizon.py | 22 +++++++++ backend/tests/test_ap24_steering_elements.py | 6 +++ backend/tests/test_ap_kernel_spine.py | 14 ++---- 7 files changed, 99 insertions(+), 15 deletions(-) diff --git a/backend/steering/methods/registry.py b/backend/steering/methods/registry.py index cdcf85a..0166636 100644 --- a/backend/steering/methods/registry.py +++ b/backend/steering/methods/registry.py @@ -56,7 +56,8 @@ def list_compatible_methods( return tuple( m for m in _METHODS.values() - if method_compatible_with_archetype( + if m.method_role == "primary" + and method_compatible_with_archetype( m, archetype_key, om_capabilities=om_capabilities ) ) diff --git a/backend/steering/strategies/next_action/agile_iteration.py b/backend/steering/strategies/next_action/agile_iteration.py index c0a3ad4..a5884be 100644 --- a/backend/steering/strategies/next_action/agile_iteration.py +++ b/backend/steering/strategies/next_action/agile_iteration.py @@ -56,7 +56,6 @@ class AgileIterationStrategy: cycle_id = str(active["id"]) from services import actions as action_service - from steering.graph.execution_engine import load_initiative_execution_graph_state actions = action_service.list_actions_for_initiative( tenant_id=ctx.tenant_id, initiative_id=initiative_id @@ -73,9 +72,22 @@ class AgileIterationStrategy: if not scoped: return [] - graph_state = load_initiative_execution_graph_state( - tenant_id=ctx.tenant_id, - initiative_id=initiative_id, + scoped_ids = {str(action["id"]) for action in scoped} + from services import execution_plan as execution_plan_service + from steering.graph.execution_engine import compute_execution_graph_state + + dependencies = execution_plan_service.list_dependencies_for_initiative( + tenant_id=ctx.tenant_id, initiative_id=initiative_id + ) + internal_dependencies = [ + dep + for dep in dependencies + if str(dep["predecessor_action_id"]) in scoped_ids + and str(dep["successor_action_id"]) in scoped_ids + ] + graph_state = compute_execution_graph_state( + actions=scoped, + dependencies=internal_dependencies, scope_roadmap_item_id=gate_scope, ) ready = build_execution_ready_candidates( diff --git a/backend/tests/factories.py b/backend/tests/factories.py index 42bbcba..581f0ec 100644 --- a/backend/tests/factories.py +++ b/backend/tests/factories.py @@ -74,6 +74,54 @@ def add_membership(*, tenant_id: str, user_id: str, tenant_role: str = "member") conn.close() +def tenant_context_from_user(user: dict): + """Build TenantContext for direct service/kernel calls in tests.""" + from psycopg2.extras import RealDictCursor + + from rights_registry import load_grants_for_roles + from tenant_context import TenantContext + + conn = get_connection() + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute( + """ + SELECT t.id, t.slug, t.name, tm.tenant_role + FROM tenant_memberships tm + JOIN tenants t ON t.id = tm.tenant_id + WHERE tm.user_id = %s AND tm.tenant_id = %s + """, + (user["id"], user["tenant_id"]), + ) + membership = cur.fetchone() + if not membership: + raise ValueError("Test user has no tenant membership") + finally: + conn.close() + + tenant_role = membership["tenant_role"] + caps = frozenset( + load_grants_for_roles( + portal_role=user["portal_role"], + tenant_role=tenant_role, + ) + ) + return TenantContext( + user_id=user["id"], + email=user["email"], + display_name=user["display_name"], + portal_role=user["portal_role"], + tenant_id=str(membership["id"]), + tenant_slug=membership["slug"], + tenant_name=membership["name"], + tenant_role=tenant_role, + actor_id=user.get("actor_id"), + actor_type="human" if user.get("actor_id") else None, + session_token="test-session", + capabilities=caps, + ) + + def provision_user_in_tenant( *, tenant_role: str = "member", diff --git a/backend/tests/test_ap22b_linear_critical_path.py b/backend/tests/test_ap22b_linear_critical_path.py index f66e66d..f514465 100644 --- a/backend/tests/test_ap22b_linear_critical_path.py +++ b/backend/tests/test_ap22b_linear_critical_path.py @@ -15,6 +15,7 @@ def test_linear_critical_path_and_next_action(client): token, title="Neue Küche", archetype_key="initiative.linear_project", + apply_starter_kit=False, ) initiative_id = created.json()["id"] diff --git a/backend/tests/test_ap22c_sequential_horizon.py b/backend/tests/test_ap22c_sequential_horizon.py index 99dad73..cc0a0aa 100644 --- a/backend/tests/test_ap22c_sequential_horizon.py +++ b/backend/tests/test_ap22c_sequential_horizon.py @@ -6,6 +6,25 @@ from tests.factories import provision_user_in_tenant from tests.test_initiatives_actions import _auth, _create_initiative, _login +_STARTER_ACTION_TITLE = "Erster Schritt — Planung konkretisieren" + + +def _remove_starter_action(client, token, initiative_id): + actions = client.get( + f"/api/initiatives/{initiative_id}/actions", + headers=_auth(token), + ) + assert actions.status_code == 200 + for action in actions.json(): + if action.get("title") == _STARTER_ACTION_TITLE: + deleted = client.delete( + f"/api/actions/{action['id']}", + headers=_auth(token), + ) + assert deleted.status_code == 204 + return + + def _gate_ids(client, token, initiative_id): roadmap = client.get( f"/api/initiatives/{initiative_id}/roadmap/items", @@ -30,6 +49,7 @@ def test_sequential_next_action_only_in_active_gate_horizon(client): ) initiative_id = created.json()["id"] g1_id, g2_id = _gate_ids(client, token, initiative_id) + _remove_starter_action(client, token, initiative_id) off_horizon = client.post( f"/api/initiatives/{initiative_id}/actions", @@ -77,6 +97,7 @@ def test_agile_on_linear_delegates_primary_without_active_sprint(client): ) initiative_id = created.json()["id"] g1_id, _ = _gate_ids(client, token, initiative_id) + _remove_starter_action(client, token, initiative_id) action = client.post( f"/api/initiatives/{initiative_id}/actions", @@ -115,6 +136,7 @@ def test_agile_on_linear_sprint_intersects_gate_horizon(client): ) initiative_id = created.json()["id"] g1_id, _ = _gate_ids(client, token, initiative_id) + _remove_starter_action(client, token, initiative_id) cycle = client.post( f"/api/initiatives/{initiative_id}/work-cycles", diff --git a/backend/tests/test_ap24_steering_elements.py b/backend/tests/test_ap24_steering_elements.py index 6f1af2a..4b1dab7 100644 --- a/backend/tests/test_ap24_steering_elements.py +++ b/backend/tests/test_ap24_steering_elements.py @@ -124,6 +124,9 @@ def test_steering_methods_filtered_by_archetype(client): def test_queue_pull_rejected_on_linear(client): + from tests.factories import provision_user_in_tenant + from tests.test_initiatives_actions import _auth, _create_initiative, _login + user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) @@ -145,6 +148,9 @@ def test_queue_pull_rejected_on_linear(client): def test_operating_context_composable_modifiers_linear(client): + from tests.factories import provision_user_in_tenant + from tests.test_initiatives_actions import _auth, _create_initiative, _login + user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) diff --git a/backend/tests/test_ap_kernel_spine.py b/backend/tests/test_ap_kernel_spine.py index 6f6aa04..493c46d 100644 --- a/backend/tests/test_ap_kernel_spine.py +++ b/backend/tests/test_ap_kernel_spine.py @@ -4,7 +4,7 @@ from __future__ import annotations from steering.kernel import apply_steering_event, evaluate_steering from steering.kernel.binding import resolve_steering_binding -from tests.factories import provision_user_in_tenant +from tests.factories import provision_user_in_tenant, tenant_context_from_user from tests.test_initiatives_actions import _auth, _create_initiative, _login @@ -20,9 +20,7 @@ def test_evaluate_steering_linear_binding(client): ) initiative_id = created.json()["id"] - from tenant_context import TenantContext - - ctx = TenantContext(tenant_id=user["tenant_id"], user_id=user["id"]) + ctx = tenant_context_from_user(user) binding = resolve_steering_binding(ctx, initiative_id) assert binding.primary_method_key == "sequential_dependency" assert binding.composition_modifier is None @@ -41,9 +39,7 @@ def test_evaluate_steering_returns_horizon_and_next(client): ) initiative_id = created.json()["id"] - from tenant_context import TenantContext - - ctx = TenantContext(tenant_id=user["tenant_id"], user_id=user["id"]) + ctx = tenant_context_from_user(user) evaluation = evaluate_steering(ctx, initiative_id=initiative_id) assert evaluation.data_source == "steering_kernel_v0.1.1" assert evaluation.lifecycle.slot_map.get("action_selection") == "active" @@ -97,9 +93,7 @@ def test_apply_steering_event_stub(client): ) initiative_id = created.json()["id"] - from tenant_context import TenantContext - - ctx = TenantContext(tenant_id=user["tenant_id"], user_id=user["id"]) + ctx = tenant_context_from_user(user) result = apply_steering_event( ctx, initiative_id=initiative_id,