ui generic rendering - base
This commit is contained in:
parent
d842884ccf
commit
7061dd4303
6 changed files with 138 additions and 84 deletions
|
|
@ -1146,6 +1146,10 @@ async def getWorkspaceMessages(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"getWorkspaceMessages: cannot read attachments for {workflowId}: {e}")
|
logger.debug(f"getWorkspaceMessages: cannot read attachments for {workflowId}: {e}")
|
||||||
|
|
||||||
|
# Resolve labels server-side as the single source of truth: every returned
|
||||||
|
# ID carries a label, and IDs whose record no longer exists are dropped
|
||||||
|
# here. The client renders what it gets without any timing-dependent
|
||||||
|
# reconciliation against its own (asynchronously loaded) source lists.
|
||||||
attachedDsLabels: Dict[str, str] = {}
|
attachedDsLabels: Dict[str, str] = {}
|
||||||
attachedFdsLabels: Dict[str, str] = {}
|
attachedFdsLabels: Dict[str, str] = {}
|
||||||
if attachedDsIds or attachedFdsIds:
|
if attachedDsIds or attachedFdsIds:
|
||||||
|
|
@ -1153,27 +1157,39 @@ async def getWorkspaceMessages(
|
||||||
rootIf = getRootInterface()
|
rootIf = getRootInterface()
|
||||||
if attachedDsIds:
|
if attachedDsIds:
|
||||||
from modules.datamodels.datamodelDataSource import DataSource
|
from modules.datamodels.datamodelDataSource import DataSource
|
||||||
|
resolvedDsIds: List[str] = []
|
||||||
for dsId in attachedDsIds:
|
for dsId in attachedDsIds:
|
||||||
try:
|
try:
|
||||||
records = rootIf.db.getRecordset(DataSource, recordFilter={"id": dsId})
|
records = rootIf.db.getRecordset(DataSource, recordFilter={"id": dsId})
|
||||||
if records:
|
except Exception as e:
|
||||||
lbl = records[0].get("label") or records[0].get("path") or ""
|
# Transient DB error: keep the id; the client falls back to
|
||||||
if lbl:
|
# its own dataSources list for the label.
|
||||||
attachedDsLabels[dsId] = str(lbl)
|
logger.warning(f"getWorkspaceMessages: label lookup failed for DataSource {dsId}: {e}")
|
||||||
except Exception:
|
resolvedDsIds.append(dsId)
|
||||||
pass
|
continue
|
||||||
|
if not records:
|
||||||
|
continue # source was deleted -- drop the stale attachment
|
||||||
|
resolvedDsIds.append(dsId)
|
||||||
|
lbl = records[0].get("label") or records[0].get("path") or ""
|
||||||
|
attachedDsLabels[dsId] = str(lbl) if lbl else dsId
|
||||||
|
attachedDsIds = resolvedDsIds
|
||||||
if attachedFdsIds:
|
if attachedFdsIds:
|
||||||
from modules.datamodels.datamodelFeatures import FeatureDataSource
|
from modules.datamodels.datamodelFeatures import FeatureDataSource
|
||||||
|
resolvedFdsIds: List[str] = []
|
||||||
for fdsId in attachedFdsIds:
|
for fdsId in attachedFdsIds:
|
||||||
try:
|
try:
|
||||||
records = rootIf.db.getRecordset(FeatureDataSource, recordFilter={"id": fdsId})
|
records = rootIf.db.getRecordset(FeatureDataSource, recordFilter={"id": fdsId})
|
||||||
if records:
|
except Exception as e:
|
||||||
tbl = records[0].get("tableName") or ""
|
logger.warning(f"getWorkspaceMessages: label lookup failed for FeatureDataSource {fdsId}: {e}")
|
||||||
lbl = records[0].get("label") or tbl
|
resolvedFdsIds.append(fdsId)
|
||||||
if lbl:
|
continue
|
||||||
attachedFdsLabels[fdsId] = str(lbl)
|
if not records:
|
||||||
except Exception:
|
continue # source was deleted -- drop the stale attachment
|
||||||
pass
|
resolvedFdsIds.append(fdsId)
|
||||||
|
tbl = records[0].get("tableName") or ""
|
||||||
|
lbl = records[0].get("label") or tbl
|
||||||
|
attachedFdsLabels[fdsId] = str(lbl) if lbl else fdsId
|
||||||
|
attachedFdsIds = resolvedFdsIds
|
||||||
|
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"messages": items,
|
"messages": items,
|
||||||
|
|
@ -1467,9 +1483,9 @@ async def listFeatureDataSources(
|
||||||
from modules.serviceCenter.core.flagResolution import buildEffectiveByWorkspaceFds
|
from modules.serviceCenter.core.flagResolution import buildEffectiveByWorkspaceFds
|
||||||
|
|
||||||
rootIf = getRootInterface()
|
rootIf = getRootInterface()
|
||||||
recordFilter: dict = {}
|
if not wsMandateId:
|
||||||
if wsMandateId:
|
return JSONResponse({"featureDataSources": []})
|
||||||
recordFilter["mandateId"] = wsMandateId
|
recordFilter: dict = {"mandateId": wsMandateId}
|
||||||
records = rootIf.db.getRecordset(FeatureDataSource, recordFilter=recordFilter) or []
|
records = rootIf.db.getRecordset(FeatureDataSource, recordFilter=recordFilter) or []
|
||||||
if not records:
|
if not records:
|
||||||
return JSONResponse({"featureDataSources": []})
|
return JSONResponse({"featureDataSources": []})
|
||||||
|
|
|
||||||
|
|
@ -375,11 +375,11 @@ class WorkflowAutomationObjects:
|
||||||
return []
|
return []
|
||||||
records = self.db.getRecordset(
|
records = self.db.getRecordset(
|
||||||
AutoRun,
|
AutoRun,
|
||||||
recordFilter={},
|
recordFilter={"workflowId": wf_ids},
|
||||||
)
|
)
|
||||||
if not records:
|
if not records:
|
||||||
return []
|
return []
|
||||||
runs = [dict(r) for r in records if r.get("workflowId") in wf_ids]
|
runs = [dict(r) for r in records]
|
||||||
wf_by_id = {w["id"]: w for w in workflows}
|
wf_by_id = {w["id"]: w for w in workflows}
|
||||||
for r in runs:
|
for r in runs:
|
||||||
wf = wf_by_id.get(r.get("workflowId"), {})
|
wf = wf_by_id.get(r.get("workflowId"), {})
|
||||||
|
|
@ -652,7 +652,7 @@ class WorkflowAutomationObjects:
|
||||||
|
|
||||||
def copyTemplateToUser(self, templateId: str) -> Optional[Dict[str, Any]]:
|
def copyTemplateToUser(self, templateId: str) -> Optional[Dict[str, Any]]:
|
||||||
"""Copy a template to a new user-owned workflow with templateScope='user'."""
|
"""Copy a template to a new user-owned workflow with templateScope='user'."""
|
||||||
template = self.getWorkflow(templateId)
|
template = self.db.getRecord(AutoWorkflow, templateId)
|
||||||
if not template or not template.get("isTemplate"):
|
if not template or not template.get("isTemplate"):
|
||||||
return None
|
return None
|
||||||
data = {
|
data = {
|
||||||
|
|
@ -672,7 +672,7 @@ class WorkflowAutomationObjects:
|
||||||
|
|
||||||
def shareTemplate(self, templateId: str, scope: str) -> Optional[Dict[str, Any]]:
|
def shareTemplate(self, templateId: str, scope: str) -> Optional[Dict[str, Any]]:
|
||||||
"""Change a template's scope. Sets sharedReadOnly=True for shared scopes, False for user scope."""
|
"""Change a template's scope. Sets sharedReadOnly=True for shared scopes, False for user scope."""
|
||||||
template = self.getWorkflow(templateId)
|
template = self.db.getRecord(AutoWorkflow, templateId)
|
||||||
if not template or not template.get("isTemplate"):
|
if not template or not template.get("isTemplate"):
|
||||||
return None
|
return None
|
||||||
updated = self.db.recordModify(AutoWorkflow, templateId, {
|
updated = self.db.recordModify(AutoWorkflow, templateId, {
|
||||||
|
|
|
||||||
|
|
@ -69,12 +69,13 @@ async def _listWorkflows(
|
||||||
try:
|
try:
|
||||||
db._ensureTableExists(AutoWorkflow)
|
db._ensureTableExists(AutoWorkflow)
|
||||||
scopeFilter = _scopedWorkflowFilter(request)
|
scopeFilter = _scopedWorkflowFilter(request)
|
||||||
if mandateId and scopeFilter is not None:
|
if mandateId:
|
||||||
if mandateId not in (scopeFilter.get("mandateId") or []):
|
scopeMandates = scopeFilter.get("mandateId") or []
|
||||||
|
if isinstance(scopeMandates, str):
|
||||||
|
scopeMandates = [scopeMandates]
|
||||||
|
if mandateId not in scopeMandates:
|
||||||
return {"items": [], "total": 0}
|
return {"items": [], "total": 0}
|
||||||
scopeFilter = {"mandateId": mandateId}
|
scopeFilter = {"mandateId": mandateId}
|
||||||
elif mandateId and scopeFilter is None:
|
|
||||||
scopeFilter = {"mandateId": mandateId}
|
|
||||||
|
|
||||||
params = _parsePaginationOr400(pagination)
|
params = _parsePaginationOr400(pagination)
|
||||||
records = db.getRecordset(AutoWorkflow, recordFilter=scopeFilter)
|
records = db.getRecordset(AutoWorkflow, recordFilter=scopeFilter)
|
||||||
|
|
@ -178,13 +179,13 @@ async def _listRuns(
|
||||||
try:
|
try:
|
||||||
db._ensureTableExists(AutoRun)
|
db._ensureTableExists(AutoRun)
|
||||||
scopeFilter = _scopedRunFilter(request)
|
scopeFilter = _scopedRunFilter(request)
|
||||||
if mandateId:
|
if mandateId and scopeFilter and "mandateId" in scopeFilter:
|
||||||
if scopeFilter is None:
|
scopeMandates = scopeFilter["mandateId"]
|
||||||
scopeFilter = {"mandateId": mandateId}
|
if isinstance(scopeMandates, str):
|
||||||
elif "mandateId" in scopeFilter:
|
scopeMandates = [scopeMandates]
|
||||||
if mandateId not in scopeFilter["mandateId"]:
|
if mandateId not in scopeMandates:
|
||||||
return {"items": [], "total": 0}
|
return {"items": [], "total": 0}
|
||||||
scopeFilter = {"mandateId": mandateId}
|
scopeFilter = {"mandateId": mandateId}
|
||||||
if workflowId:
|
if workflowId:
|
||||||
scopeFilter = {**(scopeFilter or {}), "workflowId": workflowId}
|
scopeFilter = {**(scopeFilter or {}), "workflowId": workflowId}
|
||||||
|
|
||||||
|
|
@ -242,16 +243,21 @@ async def _listTasks(
|
||||||
db = _getWorkflowAutomationDb()
|
db = _getWorkflowAutomationDb()
|
||||||
try:
|
try:
|
||||||
db._ensureTableExists(AutoTask)
|
db._ensureTableExists(AutoTask)
|
||||||
scopeFilter: Optional[Dict[str, Any]] = None
|
userId = str(request.user.id) if request.user else None
|
||||||
|
if not userId:
|
||||||
|
return {"items": [], "total": 0}
|
||||||
|
userMandateIds = _getUserMandateIds(userId)
|
||||||
|
if not userMandateIds:
|
||||||
|
return {"items": [], "total": 0}
|
||||||
|
|
||||||
if not request.isPlatformAdmin:
|
wfFilter = {"mandateId": userMandateIds, "isTemplate": False}
|
||||||
userId = str(request.user.id) if request.user else None
|
db._ensureTableExists(AutoWorkflow)
|
||||||
if not userId:
|
wfs = db.getRecordset(AutoWorkflow, recordFilter=wfFilter) or []
|
||||||
return {"items": [], "total": 0}
|
wfIds = [w.get("id") for w in wfs]
|
||||||
scopeFilter = {"assigneeId": userId}
|
scopeFilter: Dict[str, Any] = {"workflowId": wfIds} if wfIds else {"workflowId": "__none__"}
|
||||||
|
|
||||||
if status:
|
if status:
|
||||||
scopeFilter = {**(scopeFilter or {}), "status": status}
|
scopeFilter["status"] = status
|
||||||
|
|
||||||
params = _parsePaginationOr400(pagination)
|
params = _parsePaginationOr400(pagination)
|
||||||
records = db.getRecordset(AutoTask, recordFilter=scopeFilter)
|
records = db.getRecordset(AutoTask, recordFilter=scopeFilter)
|
||||||
|
|
@ -286,6 +292,27 @@ async def _listVersions(
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Editor chat history (stub — persistence not yet implemented)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@router.get("/{workflowId}/chat/messages")
|
||||||
|
async def _getEditorChatMessages(
|
||||||
|
workflowId: str,
|
||||||
|
request: RequestContext = Depends(getRequestContext),
|
||||||
|
):
|
||||||
|
db = _getWorkflowAutomationDb()
|
||||||
|
try:
|
||||||
|
db._ensureTableExists(AutoWorkflow)
|
||||||
|
wf = db.getRecord(AutoWorkflow, workflowId)
|
||||||
|
if not wf:
|
||||||
|
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||||
|
_validateWorkflowAccess(request, wf, "read")
|
||||||
|
return {"messages": []}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Step logs
|
# Step logs
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -424,9 +451,11 @@ def _listTemplates(
|
||||||
raise HTTPException(status_code=401, detail=routeApiMsg("Authentication required"))
|
raise HTTPException(status_code=401, detail=routeApiMsg("Authentication required"))
|
||||||
|
|
||||||
userId = str(context.user.id)
|
userId = str(context.user.id)
|
||||||
userMandateIds = _getUserMandateIds(userId) if not context.isPlatformAdmin else []
|
userMandateIds = _getUserMandateIds(userId)
|
||||||
|
if mandateId and mandateId not in userMandateIds:
|
||||||
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
|
||||||
effectiveMandateId = mandateId or (userMandateIds[0] if userMandateIds else None)
|
effectiveMandateId = mandateId or (userMandateIds[0] if userMandateIds else None)
|
||||||
if not effectiveMandateId and not context.isPlatformAdmin:
|
if not effectiveMandateId:
|
||||||
return {"templates": []}
|
return {"templates": []}
|
||||||
|
|
||||||
instanceId = None
|
instanceId = None
|
||||||
|
|
@ -535,8 +564,10 @@ def _copyTemplate(
|
||||||
|
|
||||||
mandateId = body.get("mandateId") if isinstance(body, dict) else None
|
mandateId = body.get("mandateId") if isinstance(body, dict) else None
|
||||||
userId = str(context.user.id)
|
userId = str(context.user.id)
|
||||||
|
userMandateIds = _getUserMandateIds(userId)
|
||||||
|
if mandateId and mandateId not in userMandateIds:
|
||||||
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
|
||||||
if not mandateId:
|
if not mandateId:
|
||||||
userMandateIds = _getUserMandateIds(userId)
|
|
||||||
mandateId = userMandateIds[0] if userMandateIds else ""
|
mandateId = userMandateIds[0] if userMandateIds else ""
|
||||||
|
|
||||||
db = _getWorkflowAutomationDb()
|
db = _getWorkflowAutomationDb()
|
||||||
|
|
@ -577,8 +608,10 @@ def _shareTemplate(
|
||||||
|
|
||||||
mandateId = body.get("mandateId", "")
|
mandateId = body.get("mandateId", "")
|
||||||
userId = str(context.user.id)
|
userId = str(context.user.id)
|
||||||
|
userMandateIds = _getUserMandateIds(userId)
|
||||||
|
if mandateId and mandateId not in userMandateIds:
|
||||||
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
|
||||||
if not mandateId:
|
if not mandateId:
|
||||||
userMandateIds = _getUserMandateIds(userId)
|
|
||||||
mandateId = userMandateIds[0] if userMandateIds else ""
|
mandateId = userMandateIds[0] if userMandateIds else ""
|
||||||
|
|
||||||
db = _getWorkflowAutomationDb()
|
db = _getWorkflowAutomationDb()
|
||||||
|
|
@ -984,14 +1017,12 @@ def _getMetrics(
|
||||||
raise HTTPException(status_code=401, detail=routeApiMsg("Authentication required"))
|
raise HTTPException(status_code=401, detail=routeApiMsg("Authentication required"))
|
||||||
|
|
||||||
userId = str(context.user.id)
|
userId = str(context.user.id)
|
||||||
userMandateIds = _getUserMandateIds(userId) if not context.isPlatformAdmin else []
|
userMandateIds = _getUserMandateIds(userId)
|
||||||
|
|
||||||
if mandateId:
|
if mandateId:
|
||||||
if not context.isPlatformAdmin and mandateId not in userMandateIds:
|
if mandateId not in userMandateIds:
|
||||||
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
|
raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
|
||||||
scopeFilter: Dict[str, Any] = {"mandateId": mandateId, "isTemplate": False}
|
scopeFilter: Dict[str, Any] = {"mandateId": mandateId, "isTemplate": False}
|
||||||
elif context.isPlatformAdmin:
|
|
||||||
scopeFilter = {"isTemplate": False}
|
|
||||||
elif userMandateIds:
|
elif userMandateIds:
|
||||||
scopeFilter = {"mandateId": userMandateIds, "isTemplate": False}
|
scopeFilter = {"mandateId": userMandateIds, "isTemplate": False}
|
||||||
else:
|
else:
|
||||||
|
|
@ -1001,13 +1032,21 @@ def _getMetrics(
|
||||||
"totalTokens": 0, "totalCredits": 0.0,
|
"totalTokens": 0, "totalCredits": 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runScope = _scopedRunFilter(context)
|
||||||
|
if mandateId and runScope:
|
||||||
|
if "mandateId" in runScope:
|
||||||
|
if mandateId not in (runScope["mandateId"] if isinstance(runScope["mandateId"], list) else [runScope["mandateId"]]):
|
||||||
|
runScope = {"mandateId": "__none__"}
|
||||||
|
else:
|
||||||
|
runScope = {"mandateId": mandateId}
|
||||||
|
|
||||||
db = _getWorkflowAutomationDb()
|
db = _getWorkflowAutomationDb()
|
||||||
try:
|
try:
|
||||||
workflows = db.getRecordset(AutoWorkflow, recordFilter=scopeFilter) or [] if db._ensureTableExists(AutoWorkflow) else []
|
workflows = (db.getRecordset(AutoWorkflow, recordFilter=scopeFilter) or []) if db._ensureTableExists(AutoWorkflow) else []
|
||||||
wfIds = [w.get("id") for w in workflows]
|
runs = (db.getRecordset(AutoRun, recordFilter=runScope) or []) if db._ensureTableExists(AutoRun) else []
|
||||||
runFilter = {"workflowId": wfIds} if wfIds else {"workflowId": "__none__"}
|
runIds = [r.get("id") for r in runs]
|
||||||
runs = db.getRecordset(AutoRun, recordFilter=runFilter) or [] if db._ensureTableExists(AutoRun) else []
|
taskFilter = {"runId": runIds} if runIds else {"runId": "__none__"}
|
||||||
tasks = db.getRecordset(AutoTask, recordFilter=runFilter) or [] if db._ensureTableExists(AutoTask) else []
|
tasks = (db.getRecordset(AutoTask, recordFilter=taskFilter) or []) if db._ensureTableExists(AutoTask) else []
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,7 @@ def _isUserMandateAdmin(userId: str, mandateId: str) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def _scopedWorkflowFilter(context: RequestContext) -> Optional[Dict[str, Any]]:
|
def _scopedWorkflowFilter(context: RequestContext) -> Optional[Dict[str, Any]]:
|
||||||
"""Build DB filter for listing workflows: mandate-scoped for members, None for sysadmin."""
|
"""Build DB filter for listing workflows: always mandate-scoped by membership."""
|
||||||
if context.isPlatformAdmin:
|
|
||||||
return None
|
|
||||||
userId = str(context.user.id) if context.user else None
|
userId = str(context.user.id) if context.user else None
|
||||||
if not userId:
|
if not userId:
|
||||||
return {"mandateId": "__impossible__"}
|
return {"mandateId": "__impossible__"}
|
||||||
|
|
@ -139,14 +137,17 @@ def _scopedWorkflowFilter(context: RequestContext) -> Optional[Dict[str, Any]]:
|
||||||
|
|
||||||
|
|
||||||
def _scopedRunFilter(context: RequestContext) -> Optional[Dict[str, Any]]:
|
def _scopedRunFilter(context: RequestContext) -> Optional[Dict[str, Any]]:
|
||||||
"""Build DB filter for listing runs: admin sees mandate runs, user sees own."""
|
"""Build DB filter for listing runs: always mandate-scoped by membership.
|
||||||
if context.isPlatformAdmin:
|
Mandate admins see all runs in their mandates, regular members see own."""
|
||||||
return None
|
|
||||||
userId = str(context.user.id) if context.user else None
|
userId = str(context.user.id) if context.user else None
|
||||||
if not userId:
|
if not userId:
|
||||||
return {"ownerId": "__impossible__"}
|
return {"ownerId": "__impossible__"}
|
||||||
mandateIds = _getUserMandateIds(userId)
|
mandateIds = _getUserMandateIds(userId)
|
||||||
|
if not mandateIds:
|
||||||
|
return {"ownerId": "__impossible__"}
|
||||||
adminMandateIds = _getAdminMandateIds(userId, mandateIds)
|
adminMandateIds = _getAdminMandateIds(userId, mandateIds)
|
||||||
|
if context.isPlatformAdmin:
|
||||||
|
return {"mandateId": mandateIds}
|
||||||
if adminMandateIds:
|
if adminMandateIds:
|
||||||
return {"mandateId": adminMandateIds}
|
return {"mandateId": adminMandateIds}
|
||||||
return {"ownerId": userId}
|
return {"ownerId": userId}
|
||||||
|
|
|
||||||
|
|
@ -2,33 +2,33 @@
|
||||||
|
|
||||||
Automated tests for the investor demo configuration.
|
Automated tests for the investor demo configuration.
|
||||||
|
|
||||||
## Prerequisites
|
## SAFETY RULE (critical)
|
||||||
|
|
||||||
1. Gateway DB must be running and accessible
|
Tests in this suite MUST be strictly read-only towards the database.
|
||||||
2. Demo config must be loaded first: Admin UI → `/admin/demo-config` → Load "Investor Demo April 2026"
|
|
||||||
3. RMA credentials must be set in `gateway/config.ini`
|
Pytest runs against the **real dev database** (there is no separate test DB).
|
||||||
|
Tests must NEVER load or remove demo configs — neither via direct module
|
||||||
|
calls (`cfg.load()` / `cfg.remove()`) nor via HTTP calls to
|
||||||
|
`/api/admin/demo-config/<code>/load|remove` (not even to assert 401/403).
|
||||||
|
|
||||||
|
Background: on 2026-06-09 a demo test reloaded `investor-demo-2026` during a
|
||||||
|
pytest run. The demo mandates (HappyLife AG, Alpina Treuhand AG) were deleted
|
||||||
|
and recreated with new UUIDs, orphaning all real feature data (trustee
|
||||||
|
accounting incl. RMA connection, workflows, documents). The data had to be
|
||||||
|
recovered by remapping ~15'000 rows across 9 databases.
|
||||||
|
|
||||||
|
Demo configs are loaded/removed manually via Admin UI → `/admin/demo-config`.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd gateway/
|
cd platform-core/
|
||||||
|
|
||||||
# All demo tests (structural, no AI calls):
|
|
||||||
pytest tests/demo/ -v
|
pytest tests/demo/ -v
|
||||||
|
|
||||||
# Only bootstrap tests:
|
|
||||||
pytest tests/demo/test_demo_bootstrap.py -v
|
|
||||||
|
|
||||||
# Only UC1 trustee:
|
|
||||||
pytest tests/demo/test_demo_uc1_trustee.py -v
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test files
|
## Test files
|
||||||
|
|
||||||
| File | What it tests |
|
| File | What it tests |
|
||||||
|------|--------------|
|
|------|--------------|
|
||||||
| `test_demo_bootstrap.py` | Idempotent load/remove, mandates, user, features, RMA, neutralization |
|
| `test_demo_api.py` | Config auto-discovery (read-only), list endpoint rejects unauthenticated |
|
||||||
| `test_demo_uc1_trustee.py` | Trustee instances, RMA config, system workflow templates |
|
| `test_demo_data_files.py` | Demo data files exist in `demoData/` (filesystem only) |
|
||||||
| `test_demo_uc2_realestate.py` | Workspace instances for agent demo |
|
|
||||||
| `test_demo_uc4_i18n.py` | i18n readiness, Spanish not pre-installed |
|
|
||||||
| `test_demo_neutralization.py` | Neutralization config enabled, test PDF exists |
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,13 @@ class TestDemoConfigDiscovery:
|
||||||
|
|
||||||
|
|
||||||
class TestDemoConfigApiEndpoints:
|
class TestDemoConfigApiEndpoints:
|
||||||
"""Test API endpoints via TestClient."""
|
"""Test API endpoints via TestClient.
|
||||||
|
|
||||||
|
SAFETY: Never call the load/remove endpoints here - not even to assert
|
||||||
|
401/403. Tests run against the real dev database; if auth/CSRF ever lets
|
||||||
|
a request through, demo mandates get deleted and recreated with new UUIDs,
|
||||||
|
orphaning all real feature data (happened on 2026-06-09).
|
||||||
|
"""
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
def client(self):
|
def client(self):
|
||||||
|
|
@ -55,11 +61,3 @@ class TestDemoConfigApiEndpoints:
|
||||||
def test_listEndpointRejectsUnauthenticated(self, client):
|
def test_listEndpointRejectsUnauthenticated(self, client):
|
||||||
response = client.get("/api/admin/demo-config")
|
response = client.get("/api/admin/demo-config")
|
||||||
assert response.status_code in (401, 403)
|
assert response.status_code in (401, 403)
|
||||||
|
|
||||||
def test_loadEndpointRejectsUnauthenticated(self, client):
|
|
||||||
response = client.post("/api/admin/demo-config/investor-demo-2026/load")
|
|
||||||
assert response.status_code in (401, 403)
|
|
||||||
|
|
||||||
def test_removeEndpointRejectsUnauthenticated(self, client):
|
|
||||||
response = client.post("/api/admin/demo-config/investor-demo-2026/remove")
|
|
||||||
assert response.status_code in (401, 403)
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue