AP2.3a: Operating Context API mit ui_profile_json und Archetyp-Sync.
Some checks failed
Test Suite / pytest-backend (push) Waiting to run
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Has been cancelled
Some checks failed
Test Suite / pytest-backend (push) Waiting to run
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Has been cancelled
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ad6740ffa8
commit
4ad908bfbf
|
|
@ -7,6 +7,7 @@ from typing import Any, Optional
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
from db import get_connection
|
from db import get_connection
|
||||||
|
from entity_archetypes.ui_profiles import INITIATIVE_UI_PROFILES, resolve_ui_profile
|
||||||
|
|
||||||
# initiative suffix → default method (shared by initiative + project mirror)
|
# initiative suffix → default method (shared by initiative + project mirror)
|
||||||
_METHOD_BY_SUFFIX: dict[str, str] = {
|
_METHOD_BY_SUFFIX: dict[str, str] = {
|
||||||
|
|
@ -70,15 +71,18 @@ def _build_specs() -> tuple[dict[str, Any], ...]:
|
||||||
specs: list[dict[str, Any]] = []
|
specs: list[dict[str, Any]] = []
|
||||||
for suffix, label, description in _INITIATIVE_SPEC_META:
|
for suffix, label, description in _INITIATIVE_SPEC_META:
|
||||||
method = _METHOD_BY_SUFFIX[suffix]
|
method = _METHOD_BY_SUFFIX[suffix]
|
||||||
|
initiative_key = f"initiative.{suffix}"
|
||||||
|
ui_profile = INITIATIVE_UI_PROFILES.get(initiative_key, {})
|
||||||
specs.append(
|
specs.append(
|
||||||
{
|
{
|
||||||
"key": f"initiative.{suffix}",
|
"key": initiative_key,
|
||||||
"entity_type": "initiative",
|
"entity_type": "initiative",
|
||||||
"label": label,
|
"label": label,
|
||||||
"description": description,
|
"description": description,
|
||||||
"is_system": True,
|
"is_system": True,
|
||||||
"default_method_key": method,
|
"default_method_key": method,
|
||||||
"default_method_profile_key": None,
|
"default_method_profile_key": None,
|
||||||
|
"ui_profile_json": ui_profile,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
specs.append(
|
specs.append(
|
||||||
|
|
@ -90,6 +94,7 @@ def _build_specs() -> tuple[dict[str, Any], ...]:
|
||||||
"is_system": True,
|
"is_system": True,
|
||||||
"default_method_key": method,
|
"default_method_key": method,
|
||||||
"default_method_profile_key": None,
|
"default_method_profile_key": None,
|
||||||
|
"ui_profile_json": {},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return tuple(specs)
|
return tuple(specs)
|
||||||
|
|
@ -189,6 +194,14 @@ def resolve_default_method_key(archetype_key: str) -> str:
|
||||||
return spec.get("default_method_key", "generic_operating")
|
return spec.get("default_method_key", "generic_operating")
|
||||||
|
|
||||||
|
|
||||||
|
def get_ui_profile_json(archetype_key: str) -> dict[str, Any]:
|
||||||
|
"""UI-Profil aus Registry-Seed (DB-Sync-Quelle)."""
|
||||||
|
spec = _KEY_INDEX.get(archetype_key)
|
||||||
|
if spec and spec.get("ui_profile_json"):
|
||||||
|
return dict(spec["ui_profile_json"])
|
||||||
|
return resolve_ui_profile(archetype_key)
|
||||||
|
|
||||||
|
|
||||||
def mirror_project_archetype_key(initiative_archetype_key: str) -> str:
|
def mirror_project_archetype_key(initiative_archetype_key: str) -> str:
|
||||||
if initiative_archetype_key.startswith("initiative."):
|
if initiative_archetype_key.startswith("initiative."):
|
||||||
suffix = initiative_archetype_key[len("initiative.") :]
|
suffix = initiative_archetype_key[len("initiative.") :]
|
||||||
|
|
|
||||||
181
backend/entity_archetypes/ui_profiles.py
Normal file
181
backend/entity_archetypes/ui_profiles.py
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
"""UI-Profile für Initiative-Archetypen — migriert aus methodUiDefaults.js (AP2.3a)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_PRODUCT_PROCESS = (
|
||||||
|
{"key": "inbox", "to": "/plan/inbox", "label": "Eingang", "mode": "plan"},
|
||||||
|
{"key": "sprint-plan", "to": "/plan/sprint", "label": "Sprint planen", "mode": "plan"},
|
||||||
|
{"key": "sprint-work", "to": "/work/sprint", "label": "Ausführen", "mode": "work"},
|
||||||
|
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
|
||||||
|
)
|
||||||
|
|
||||||
|
_LINEAR_PROCESS = (
|
||||||
|
{"key": "gates", "to": "/plan/gates", "label": "Zielzustände", "mode": "plan"},
|
||||||
|
{"key": "work", "to": "/work/today", "label": "Ausführen", "mode": "work"},
|
||||||
|
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
|
||||||
|
)
|
||||||
|
|
||||||
|
_MATURITY_PROCESS = (
|
||||||
|
{"key": "gates", "to": "/plan/gates", "label": "Stufen", "mode": "plan"},
|
||||||
|
{"key": "journey", "to": "/control/journey", "label": "Rhythmen", "mode": "control"},
|
||||||
|
{"key": "work", "to": "/work/today", "label": "Ausführen", "mode": "work"},
|
||||||
|
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
|
||||||
|
)
|
||||||
|
|
||||||
|
_PROGRAM_PROCESS = (
|
||||||
|
{"key": "gates", "to": "/plan/gates", "label": "Phasen", "mode": "plan"},
|
||||||
|
{"key": "inbox", "to": "/plan/inbox", "label": "Eingang", "mode": "plan"},
|
||||||
|
{"key": "work", "to": "/work/today", "label": "Ausführen", "mode": "work"},
|
||||||
|
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
|
||||||
|
)
|
||||||
|
|
||||||
|
_SUPPORT_QUEUE_PROCESS = (
|
||||||
|
{"key": "inbox", "to": "/plan/inbox", "label": "Eingang", "mode": "plan"},
|
||||||
|
{"key": "work", "to": "/work/today", "label": "Ausführen", "mode": "work"},
|
||||||
|
{"key": "control", "to": "/control/status", "label": "Kontrolle", "mode": "control"},
|
||||||
|
)
|
||||||
|
|
||||||
|
_COMMON_OM_SLICES = (
|
||||||
|
"actions",
|
||||||
|
"blockers",
|
||||||
|
"evidence",
|
||||||
|
"decisions",
|
||||||
|
"reviews",
|
||||||
|
"steering_methods",
|
||||||
|
)
|
||||||
|
|
||||||
|
GENERIC_UI_PROFILE: dict[str, Any] = {
|
||||||
|
"processSteps": [],
|
||||||
|
"planDefaultRoute": "/plan/profile",
|
||||||
|
"workDefaultRoute": "/work/today",
|
||||||
|
"controlDefaultRoute": "/control/status",
|
||||||
|
"planOutlineKeys": None,
|
||||||
|
"workNavKeys": None,
|
||||||
|
"dataSlices": ["actions", "backlog", "roadmap", "projects", "blockers", *_COMMON_OM_SLICES[1:]],
|
||||||
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
}
|
||||||
|
|
||||||
|
INITIATIVE_UI_PROFILES: dict[str, dict[str, Any]] = {
|
||||||
|
"initiative.product": {
|
||||||
|
"processSteps": list(_PRODUCT_PROCESS),
|
||||||
|
"planDefaultRoute": "/plan/inbox",
|
||||||
|
"workDefaultRoute": "/work/sprint",
|
||||||
|
"controlDefaultRoute": "/control/status",
|
||||||
|
"planOutlineKeys": ["profile", "inbox", "sprint", "gates"],
|
||||||
|
"workNavKeys": ["sprint", "today", "mine"],
|
||||||
|
"dataSlices": [
|
||||||
|
"backlog",
|
||||||
|
"actions",
|
||||||
|
"roadmap",
|
||||||
|
"work_cycles",
|
||||||
|
"projects",
|
||||||
|
"blockers",
|
||||||
|
"evidence",
|
||||||
|
"decisions",
|
||||||
|
"reviews",
|
||||||
|
"steering_methods",
|
||||||
|
],
|
||||||
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
},
|
||||||
|
"initiative.linear_project": {
|
||||||
|
"processSteps": list(_LINEAR_PROCESS),
|
||||||
|
"planDefaultRoute": "/plan/gates",
|
||||||
|
"workDefaultRoute": "/work/today",
|
||||||
|
"controlDefaultRoute": "/control/status",
|
||||||
|
"planOutlineKeys": ["profile", "structure", "gates", "inbox", "work"],
|
||||||
|
"workNavKeys": ["today", "mine", "sprint"],
|
||||||
|
"dataSlices": [
|
||||||
|
"backlog",
|
||||||
|
"actions",
|
||||||
|
"roadmap",
|
||||||
|
"projects",
|
||||||
|
"blockers",
|
||||||
|
"evidence",
|
||||||
|
"decisions",
|
||||||
|
"reviews",
|
||||||
|
"steering_methods",
|
||||||
|
],
|
||||||
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
},
|
||||||
|
"initiative.maturity_journey": {
|
||||||
|
"processSteps": list(_MATURITY_PROCESS),
|
||||||
|
"planDefaultRoute": "/plan/gates",
|
||||||
|
"workDefaultRoute": "/work/today",
|
||||||
|
"controlDefaultRoute": "/control/status",
|
||||||
|
"planOutlineKeys": ["profile", "gates", "inbox", "work"],
|
||||||
|
"workNavKeys": ["today", "mine", "sprint"],
|
||||||
|
"dataSlices": [
|
||||||
|
"actions",
|
||||||
|
"roadmap",
|
||||||
|
"blockers",
|
||||||
|
"evidence",
|
||||||
|
"decisions",
|
||||||
|
"reviews",
|
||||||
|
"recurring",
|
||||||
|
"steering_methods",
|
||||||
|
],
|
||||||
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
},
|
||||||
|
"initiative.program": {
|
||||||
|
"processSteps": list(_PROGRAM_PROCESS),
|
||||||
|
"planDefaultRoute": "/plan/gates",
|
||||||
|
"workDefaultRoute": "/work/today",
|
||||||
|
"controlDefaultRoute": "/control/status",
|
||||||
|
"planOutlineKeys": ["profile", "structure", "gates", "inbox", "sprint", "work"],
|
||||||
|
"workNavKeys": ["today", "mine", "sprint"],
|
||||||
|
"dataSlices": [
|
||||||
|
"backlog",
|
||||||
|
"actions",
|
||||||
|
"roadmap",
|
||||||
|
"projects",
|
||||||
|
"work_cycles",
|
||||||
|
"blockers",
|
||||||
|
"evidence",
|
||||||
|
"decisions",
|
||||||
|
"reviews",
|
||||||
|
"steering_methods",
|
||||||
|
],
|
||||||
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
},
|
||||||
|
"initiative.support_queue": {
|
||||||
|
"processSteps": list(_SUPPORT_QUEUE_PROCESS),
|
||||||
|
"planDefaultRoute": "/plan/inbox",
|
||||||
|
"workDefaultRoute": "/work/today",
|
||||||
|
"controlDefaultRoute": "/control/status",
|
||||||
|
"planOutlineKeys": ["profile", "inbox", "work"],
|
||||||
|
"workNavKeys": ["today", "mine"],
|
||||||
|
"dataSlices": [
|
||||||
|
"backlog",
|
||||||
|
"actions",
|
||||||
|
"blockers",
|
||||||
|
"steering_methods",
|
||||||
|
],
|
||||||
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
},
|
||||||
|
"initiative.recurring_program": {
|
||||||
|
"processSteps": list(_MATURITY_PROCESS),
|
||||||
|
"planDefaultRoute": "/plan/gates",
|
||||||
|
"workDefaultRoute": "/work/today",
|
||||||
|
"controlDefaultRoute": "/control/status",
|
||||||
|
"planOutlineKeys": ["profile", "gates", "inbox", "work"],
|
||||||
|
"workNavKeys": ["today", "mine"],
|
||||||
|
"dataSlices": [
|
||||||
|
"actions",
|
||||||
|
"roadmap",
|
||||||
|
"recurring",
|
||||||
|
"blockers",
|
||||||
|
"steering_methods",
|
||||||
|
],
|
||||||
|
"enabledModes": ["plan", "work", "control"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_ui_profile(archetype_key: str) -> dict[str, Any]:
|
||||||
|
"""UI-Profil aus Code-Seed; unbekannte Archetypen → Generic-Fallback."""
|
||||||
|
profile = INITIATIVE_UI_PROFILES.get(archetype_key)
|
||||||
|
if profile:
|
||||||
|
return dict(profile)
|
||||||
|
return dict(GENERIC_UI_PROFILE)
|
||||||
|
|
@ -14,17 +14,20 @@ def sync_entity_field_registry_to_db() -> int:
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
for archetype in ENTITY_ARCHETYPES:
|
for archetype in ENTITY_ARCHETYPES:
|
||||||
|
ui_profile = archetype.get("ui_profile_json") or {}
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO entity_archetypes (
|
INSERT INTO entity_archetypes (
|
||||||
archetype_key, entity_type, label, description, is_system
|
archetype_key, entity_type, label, description, is_system,
|
||||||
|
ui_profile_json
|
||||||
)
|
)
|
||||||
VALUES (%s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s::jsonb)
|
||||||
ON CONFLICT (archetype_key) DO UPDATE SET
|
ON CONFLICT (archetype_key) DO UPDATE SET
|
||||||
entity_type = EXCLUDED.entity_type,
|
entity_type = EXCLUDED.entity_type,
|
||||||
label = EXCLUDED.label,
|
label = EXCLUDED.label,
|
||||||
description = EXCLUDED.description,
|
description = EXCLUDED.description,
|
||||||
is_system = EXCLUDED.is_system,
|
is_system = EXCLUDED.is_system,
|
||||||
|
ui_profile_json = EXCLUDED.ui_profile_json,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
|
|
@ -33,6 +36,7 @@ def sync_entity_field_registry_to_db() -> int:
|
||||||
archetype["label"],
|
archetype["label"],
|
||||||
archetype["description"],
|
archetype["description"],
|
||||||
archetype["is_system"],
|
archetype["is_system"],
|
||||||
|
json.dumps(ui_profile),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
4
backend/migrations/027_entity_archetype_ui_profile.sql
Normal file
4
backend/migrations/027_entity_archetype_ui_profile.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
-- AP2.3a: UI-Profil pro Archetyp (Registry-Sync → Frontend Operating Context)
|
||||||
|
|
||||||
|
ALTER TABLE entity_archetypes
|
||||||
|
ADD COLUMN IF NOT EXISTS ui_profile_json JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||||
|
|
@ -21,6 +21,7 @@ from services import blockers as blocker_service
|
||||||
from services import decisions as decision_service
|
from services import decisions as decision_service
|
||||||
from services import evidence as evidence_service
|
from services import evidence as evidence_service
|
||||||
from services import initiatives as initiative_service
|
from services import initiatives as initiative_service
|
||||||
|
from services import operating_context as operating_context_service
|
||||||
from services import milestones as milestone_service
|
from services import milestones as milestone_service
|
||||||
from services import recurring as recurring_service
|
from services import recurring as recurring_service
|
||||||
from services import reviews as review_service
|
from services import reviews as review_service
|
||||||
|
|
@ -213,6 +214,19 @@ def get_initiative_steering_context(
|
||||||
return get_steering_context_dto(ctx, initiative_id=initiative_id)
|
return get_steering_context_dto(ctx, initiative_id=initiative_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{initiative_id}/operating-context")
|
||||||
|
def get_initiative_operating_context(
|
||||||
|
initiative_id: str,
|
||||||
|
ctx: TenantContext = Depends(require_capability("kairo.initiative.read")),
|
||||||
|
):
|
||||||
|
context = operating_context_service.get_operating_context(
|
||||||
|
tenant_id=ctx.tenant_id, initiative_id=initiative_id
|
||||||
|
)
|
||||||
|
if not context:
|
||||||
|
raise HTTPException(status_code=404, detail="Vorhaben nicht gefunden")
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
def _require_initiative(*, tenant_id: str, initiative_id: str) -> dict:
|
def _require_initiative(*, tenant_id: str, initiative_id: str) -> dict:
|
||||||
initiative = initiative_service.get_initiative(
|
initiative = initiative_service.get_initiative(
|
||||||
tenant_id=tenant_id, initiative_id=initiative_id
|
tenant_id=tenant_id, initiative_id=initiative_id
|
||||||
|
|
|
||||||
108
backend/services/operating_context.py
Normal file
108
backend/services/operating_context.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
"""Operating Context read model — AP2.3a."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
|
from db import get_connection
|
||||||
|
from entity_archetypes.registry import get_ui_profile_json, resolve_ui_profile
|
||||||
|
from services.initiatives import get_initiative
|
||||||
|
from services.steering_context import ensure_steering_context, get_steering_context
|
||||||
|
from steering.methods.registry import get_method
|
||||||
|
|
||||||
|
|
||||||
|
def _load_ui_profile_from_db(archetype_key: str) -> Optional[dict[str, Any]]:
|
||||||
|
conn = get_connection()
|
||||||
|
try:
|
||||||
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT ui_profile_json
|
||||||
|
FROM entity_archetypes
|
||||||
|
WHERE archetype_key = %s
|
||||||
|
""",
|
||||||
|
(archetype_key,),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
profile = row.get("ui_profile_json")
|
||||||
|
if isinstance(profile, dict) and profile:
|
||||||
|
return dict(profile)
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_ui_profile(archetype_key: str) -> dict[str, Any]:
|
||||||
|
db_profile = _load_ui_profile_from_db(archetype_key)
|
||||||
|
if db_profile:
|
||||||
|
return db_profile
|
||||||
|
seed_profile = get_ui_profile_json(archetype_key)
|
||||||
|
if seed_profile:
|
||||||
|
return seed_profile
|
||||||
|
return resolve_ui_profile(archetype_key)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_data_slices(
|
||||||
|
*,
|
||||||
|
ui_profile: dict[str, Any],
|
||||||
|
method_key: str,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Schnittmenge Archetyp-Default ∩ Method-Slices (Phase E erweitert)."""
|
||||||
|
archetype_slices = ui_profile.get("dataSlices") or []
|
||||||
|
method = get_method(method_key)
|
||||||
|
if method and getattr(method, "data_slices", None):
|
||||||
|
method_slices = set(method.data_slices)
|
||||||
|
return [s for s in archetype_slices if s in method_slices]
|
||||||
|
return list(archetype_slices)
|
||||||
|
|
||||||
|
|
||||||
|
def _method_capabilities(method_key: str) -> dict[str, Any]:
|
||||||
|
method = get_method(method_key)
|
||||||
|
if not method:
|
||||||
|
return {
|
||||||
|
"lifecycle_steps": [],
|
||||||
|
"next_action_strategy_key": "default",
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"lifecycle_steps": list(method.default_lifecycle_steps),
|
||||||
|
"next_action_strategy_key": method.next_action_strategy_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_operating_context(
|
||||||
|
*, tenant_id: str, initiative_id: str
|
||||||
|
) -> Optional[dict[str, Any]]:
|
||||||
|
initiative = get_initiative(tenant_id=tenant_id, initiative_id=initiative_id)
|
||||||
|
if not initiative:
|
||||||
|
return None
|
||||||
|
|
||||||
|
steering = get_steering_context(tenant_id=tenant_id, initiative_id=initiative_id)
|
||||||
|
if not steering:
|
||||||
|
steering = ensure_steering_context(
|
||||||
|
tenant_id=tenant_id, initiative_id=initiative_id
|
||||||
|
)
|
||||||
|
|
||||||
|
archetype_key = initiative["archetype_key"]
|
||||||
|
ui_profile = _resolve_ui_profile(archetype_key)
|
||||||
|
method_key = steering["method_key"]
|
||||||
|
metadata = steering.get("lifecycle_metadata") or {}
|
||||||
|
if isinstance(metadata, str):
|
||||||
|
metadata = {}
|
||||||
|
method_profile_key = metadata.get("method_profile_key")
|
||||||
|
data_slices = _resolve_data_slices(ui_profile=ui_profile, method_key=method_key)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"initiative_id": initiative_id,
|
||||||
|
"archetype_key": archetype_key,
|
||||||
|
"method_key": method_key,
|
||||||
|
"method_profile_key": method_profile_key,
|
||||||
|
"ui_profile": ui_profile,
|
||||||
|
"data_slices": data_slices,
|
||||||
|
"method_capabilities": _method_capabilities(method_key),
|
||||||
|
}
|
||||||
159
backend/tests/test_ap23a_operating_context.py
Normal file
159
backend/tests/test_ap23a_operating_context.py
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
"""AP2.3a — Operating Context API."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from auth import AUTH_HEADER
|
||||||
|
from tests.factories import provision_user_in_tenant
|
||||||
|
from tests.test_initiatives_actions import _auth, _create_initiative, _login
|
||||||
|
|
||||||
|
|
||||||
|
def test_operating_context_product(client):
|
||||||
|
user = provision_user_in_tenant(tenant_role="member")
|
||||||
|
token = _login(client, user)
|
||||||
|
|
||||||
|
created = _create_initiative(
|
||||||
|
client,
|
||||||
|
token,
|
||||||
|
title="Product Alpha",
|
||||||
|
archetype_key="initiative.product",
|
||||||
|
)
|
||||||
|
assert created.status_code == 201
|
||||||
|
initiative_id = created.json()["id"]
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/operating-context",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert res.status_code == 200
|
||||||
|
body = res.json()
|
||||||
|
assert body["initiative_id"] == initiative_id
|
||||||
|
assert body["archetype_key"] == "initiative.product"
|
||||||
|
assert body["method_key"] == "continuous_product"
|
||||||
|
assert body["ui_profile"]["planDefaultRoute"] == "/plan/inbox"
|
||||||
|
assert body["ui_profile"]["workDefaultRoute"] == "/work/sprint"
|
||||||
|
assert "work_cycles" in body["data_slices"]
|
||||||
|
assert body["ui_profile"]["processSteps"]
|
||||||
|
assert body["method_capabilities"]["next_action_strategy_key"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_operating_context_linear(client):
|
||||||
|
user = provision_user_in_tenant(tenant_role="member")
|
||||||
|
token = _login(client, user)
|
||||||
|
|
||||||
|
created = _create_initiative(
|
||||||
|
client,
|
||||||
|
token,
|
||||||
|
title="Linear Proj",
|
||||||
|
archetype_key="initiative.linear_project",
|
||||||
|
)
|
||||||
|
initiative_id = created.json()["id"]
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/operating-context",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert res.status_code == 200
|
||||||
|
body = res.json()
|
||||||
|
assert body["method_key"] == "sequential_dependency"
|
||||||
|
assert body["ui_profile"]["planDefaultRoute"] == "/plan/gates"
|
||||||
|
assert "work_cycles" not in body["data_slices"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_operating_context_maturity(client):
|
||||||
|
user = provision_user_in_tenant(tenant_role="member")
|
||||||
|
token = _login(client, user)
|
||||||
|
|
||||||
|
created = _create_initiative(
|
||||||
|
client,
|
||||||
|
token,
|
||||||
|
title="Maturity",
|
||||||
|
archetype_key="initiative.maturity_journey",
|
||||||
|
)
|
||||||
|
initiative_id = created.json()["id"]
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/operating-context",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert res.status_code == 200
|
||||||
|
body = res.json()
|
||||||
|
assert body["method_key"] == "maturity_progression"
|
||||||
|
assert any(s["key"] == "journey" for s in body["ui_profile"]["processSteps"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_operating_context_program(client):
|
||||||
|
user = provision_user_in_tenant(tenant_role="member")
|
||||||
|
token = _login(client, user)
|
||||||
|
|
||||||
|
created = _create_initiative(
|
||||||
|
client,
|
||||||
|
token,
|
||||||
|
title="Program",
|
||||||
|
archetype_key="initiative.program",
|
||||||
|
)
|
||||||
|
initiative_id = created.json()["id"]
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/operating-context",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert res.status_code == 200
|
||||||
|
body = res.json()
|
||||||
|
assert body["method_key"] == "program_delivery"
|
||||||
|
assert body["ui_profile"]["planOutlineKeys"] is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_operating_context_generic_fallback(client):
|
||||||
|
user = provision_user_in_tenant(tenant_role="member")
|
||||||
|
token = _login(client, user)
|
||||||
|
|
||||||
|
created = _create_initiative(
|
||||||
|
client,
|
||||||
|
token,
|
||||||
|
title="Dispute",
|
||||||
|
archetype_key="initiative.dispute_case",
|
||||||
|
)
|
||||||
|
initiative_id = created.json()["id"]
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/operating-context",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert res.status_code == 200
|
||||||
|
body = res.json()
|
||||||
|
assert body["ui_profile"]["planDefaultRoute"] == "/plan/profile"
|
||||||
|
assert body["ui_profile"]["processSteps"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_operating_context_support_queue_no_work_cycles(client):
|
||||||
|
user = provision_user_in_tenant(tenant_role="member")
|
||||||
|
token = _login(client, user)
|
||||||
|
|
||||||
|
created = _create_initiative(
|
||||||
|
client,
|
||||||
|
token,
|
||||||
|
title="Support Queue",
|
||||||
|
archetype_key="initiative.support_queue",
|
||||||
|
)
|
||||||
|
initiative_id = created.json()["id"]
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
f"/api/initiatives/{initiative_id}/operating-context",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert res.status_code == 200
|
||||||
|
body = res.json()
|
||||||
|
assert body["method_key"] == "queue_pull"
|
||||||
|
assert "work_cycles" not in body["data_slices"]
|
||||||
|
assert "sprint" not in (body["ui_profile"].get("planOutlineKeys") or [])
|
||||||
|
|
||||||
|
|
||||||
|
def test_operating_context_not_found(client):
|
||||||
|
user = provision_user_in_tenant(tenant_role="member")
|
||||||
|
token = _login(client, user)
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
"/api/initiatives/00000000-0000-0000-0000-000000000099/operating-context",
|
||||||
|
headers=_auth(token),
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
||||||
Loading…
Reference in New Issue
Block a user