fix(imports): 3 broken imports — ENTITY_MODELS path, Room→CommConversation, get_cached_mail_summary→MailService
Check Cross-Plugin Imports / check (push) Has been cancelled

This commit is contained in:
Agent Zero
2026-08-17 07:14:29 +02:00
parent d50615e870
commit 5b0b1e093a
19 changed files with 15296 additions and 4 deletions
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""Test Marathon — Runs all trace scripts and test scripts, combines results.
Usage: python3 scripts/test_suite/marathon.py
"""
from __future__ import annotations
import json
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
SUITE_DIR = Path(__file__).resolve().parent
# All trace scripts (static analysis — find verkabelungsfehler)
TRACE_SCRIPTS = [
("trace_api_contracts", "Frontend↔Backend API Contracts"),
("trace_hooks", "Hook Registrations vs Triggers"),
("trace_functions", "Dead Functions (defined but never called)"),
("trace_stores", "Unused Store Actions/State"),
("trace_contracts", "Contract Attribute Mismatches"),
("trace_plugins", "Plugin→Manifest→Frontend Verkabelung"),
("trace_imports", "Broken/Missing Imports"),
]
# All test scripts (runtime — find crashes and errors)
# These will be added as they are built
TEST_SCRIPTS = [
# ("test_all_endpoints", "All 224 API Endpoints"),
# ("test_all_routes", "All 62 Frontend Routes"),
# ("test_schema_vs_models", "DB Schema vs Models"),
# ("test_permission_matrix", "Permission Matrix"),
]
def run_script(name: str, description: str) -> dict:
"""Run a single script and capture results."""
script_path = SUITE_DIR / f"{name}.py"
if not script_path.exists():
return {"name": name, "description": description, "status": "SKIP", "reason": "Script not found", "issues": 0}
print(f"\n{'' * 70}")
print(f"RUNNING: {name}{description}")
print(f"{'' * 70}")
start = time.time()
try:
result = subprocess.run(
[sys.executable, str(script_path)],
capture_output=True,
text=True,
timeout=300,
cwd=str(ROOT),
)
elapsed = time.time() - start
# Print stdout
if result.stdout:
print(result.stdout)
if result.stderr:
print(f"STDERR: {result.stderr[:500]}")
# Try to read results JSON
results_file = SUITE_DIR / f"results_{name}.json"
issues_count = 0
if results_file.exists():
try:
with open(results_file) as f:
data = json.load(f)
if "summary" in data:
issues_count = data["summary"].get("total_issues", 0)
elif "issues" in data:
issues_count = len(data["issues"])
elif "mismatches" in data:
issues_count = len(data["mismatches"])
elif "broken" in data:
issues_count = len(data["broken"])
elif "critical_dead" in data:
issues_count = len(data["critical_dead"])
elif "orphan_registrations" in data:
issues_count = len(data.get("orphan_registrations", [])) + len(data.get("orphan_triggers", []))
elif "unused" in data:
issues_count = sum(len(v) for v in data["unused"].values()) if isinstance(data["unused"], dict) else 0
except Exception:
pass
return {
"name": name,
"description": description,
"status": "PASS" if result.returncode == 0 else "FAIL",
"exit_code": result.returncode,
"elapsed": round(elapsed, 2),
"issues": issues_count,
}
except subprocess.TimeoutExpired:
elapsed = time.time() - start
return {"name": name, "description": description, "status": "TIMEOUT", "elapsed": round(elapsed, 2), "issues": 0}
except Exception as e:
elapsed = time.time() - start
return {"name": name, "description": description, "status": "ERROR", "error": str(e), "elapsed": round(elapsed, 2), "issues": 0}
def main():
print("" + "" * 70 + "")
print("" + " LEOCRM TEST MARATHON — Vollständige System-Audit".center(70) + "")
print("" + f" Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}".center(70) + "")
print("" + "" * 70 + "")
all_results = []
total_start = time.time()
# Phase 1: Trace Scripts (static analysis)
print("\n" + "=" * 70)
print("PHASE 1: TRACE SCRIPTS — Statische Verkabelungs-Analyse")
print("=" * 70)
for name, desc in TRACE_SCRIPTS:
result = run_script(name, desc)
all_results.append(result)
# Phase 2: Test Scripts (runtime)
if TEST_SCRIPTS:
print("\n" + "=" * 70)
print("PHASE 2: TEST SCRIPTS — Runtime Tests")
print("=" * 70)
for name, desc in TEST_SCRIPTS:
result = run_script(name, desc)
all_results.append(result)
# Summary
total_elapsed = time.time() - total_start
print("\n" + "" + "" * 70 + "")
print("" + " MARATHON SUMMARY".center(70) + "")
print("" + "" * 70 + "")
total_issues = 0
passed = 0
failed = 0
skipped = 0
for r in all_results:
status_icon = {"PASS": "", "FAIL": "", "SKIP": "⏭️", "TIMEOUT": "", "ERROR": "💥"}.get(r["status"], "?")
issues = r.get("issues", 0)
total_issues += issues
if r["status"] == "PASS":
passed += 1
elif r["status"] in ("FAIL", "ERROR", "TIMEOUT"):
failed += 1
else:
skipped += 1
elapsed = r.get("elapsed", 0)
line = f" {status_icon} {r['name']:<30} {issues:>5} issues {elapsed:>6}s"
print("" + line.ljust(70) + "")
print("" + "" * 70 + "")
summary_line = f" Total: {len(all_results)} scripts | ✅ {passed} passed | ❌ {failed} failed | ⏭️ {skipped} skipped"
print("" + summary_line.ljust(70) + "")
issues_line = f" Total issues found: {total_issues}"
print("" + issues_line.ljust(70) + "")
time_line = f" Total time: {round(total_elapsed, 2)}s"
print("" + time_line.ljust(70) + "")
print("" + "" * 70 + "")
# Write combined results
combined_file = SUITE_DIR / "marathon_results.json"
with open(combined_file, "w") as f:
json.dump({
"timestamp": datetime.now().isoformat(),
"total_time": round(total_elapsed, 2),
"total_scripts": len(all_results),
"total_issues": total_issues,
"passed": passed,
"failed": failed,
"skipped": skipped,
"results": all_results,
}, f, indent=2)
print(f"\nCombined results: {combined_file}")
return total_issues
if __name__ == "__main__":
sys.exit(main())
+67
View File
@@ -0,0 +1,67 @@
{
"timestamp": "2026-08-17T00:23:01.849835",
"total_time": 6.53,
"total_scripts": 7,
"total_issues": 1112,
"passed": 1,
"failed": 6,
"skipped": 0,
"results": [
{
"name": "trace_api_contracts",
"description": "Frontend\u2194Backend API Contracts",
"status": "FAIL",
"exit_code": 246,
"elapsed": 0.21,
"issues": 758
},
{
"name": "trace_hooks",
"description": "Hook Registrations vs Triggers",
"status": "FAIL",
"exit_code": 1,
"elapsed": 0.06,
"issues": 0
},
{
"name": "trace_functions",
"description": "Dead Functions (defined but never called)",
"status": "FAIL",
"exit_code": 3,
"elapsed": 2.94,
"issues": 3
},
{
"name": "trace_stores",
"description": "Unused Store Actions/State",
"status": "FAIL",
"exit_code": 67,
"elapsed": 0.08,
"issues": 323
},
{
"name": "trace_contracts",
"description": "Contract Attribute Mismatches",
"status": "PASS",
"exit_code": 0,
"elapsed": 1.72,
"issues": 0
},
{
"name": "trace_plugins",
"description": "Plugin\u2192Manifest\u2192Frontend Verkabelung",
"status": "FAIL",
"exit_code": 24,
"elapsed": 0.11,
"issues": 24
},
{
"name": "trace_imports",
"description": "Broken/Missing Imports",
"status": "FAIL",
"exit_code": 4,
"elapsed": 1.4,
"issues": 4
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,327 @@
{
"contracts": [
{
"name": "ContractError",
"file": "app/plugins/builtins/contracts.py",
"attributes": []
},
{
"name": "PluginContract",
"file": "app/plugins/builtins/contracts.py",
"attributes": [
"contract_name"
]
},
{
"name": "ContractRegistry",
"file": "app/plugins/builtins/contracts.py",
"attributes": [
"_instance"
]
},
{
"name": "McpClientContract",
"file": "app/plugins/builtins/mcp_client/contracts.py",
"attributes": [
"contract_name",
"McpServerConfig",
"McpClient",
"McpServerExecuteRequest",
"McpServerExecuteResponse",
"McpServerToolInfo",
"McpServerToolsResponse"
]
},
{
"name": "ForgejoErrorReporterContract",
"file": "app/plugins/builtins/forgejo_error_reporter/contracts.py",
"attributes": [
"contract_name",
"report_error_to_forgejo",
"ReportedError"
]
},
{
"name": "AutomationContract",
"file": "app/plugins/builtins/automation/contracts.py",
"attributes": [
"contract_name",
"AgentDefinition",
"Automation",
"CronJob",
"AgentRun",
"AgentVersion",
"AutomationRun",
"AutomationVersion",
"AgentService",
"AutomationService",
"CronJobService",
"RunLogService",
"run_agent",
"run_automation",
"calculate_next_run",
"scheduler_tick",
"send_agent_message"
]
},
{
"name": "DmsContract",
"file": "app/plugins/builtins/dms/contracts.py",
"attributes": [
"contract_name",
"DmsFile",
"Folder"
]
},
{
"name": "MailContract",
"file": "app/plugins/builtins/mail/contracts.py",
"attributes": [
"contract_name",
"Mail"
]
},
{
"name": "TestContractRegistry",
"file": "app/plugins/builtins/tests/test_contracts.py",
"attributes": []
},
{
"name": "TestKommunikationContract",
"file": "app/plugins/builtins/tests/test_contracts.py",
"attributes": []
},
{
"name": "TestAIAssistantContract",
"file": "app/plugins/builtins/tests/test_contracts.py",
"attributes": []
},
{
"name": "TestMailContract",
"file": "app/plugins/builtins/tests/test_contracts.py",
"attributes": []
},
{
"name": "TasksContract",
"file": "app/plugins/builtins/tasks/contracts.py",
"attributes": [
"contract_name",
"list_tasks",
"get_task",
"create_task",
"update_task",
"delete_task",
"assign_task",
"update_task_status",
"get_due_tasks",
"Task"
]
},
{
"name": "CalendarContract",
"file": "app/plugins/builtins/calendar/contracts.py",
"attributes": [
"contract_name",
"Calendar",
"CalendarEntry",
"CalendarEntryLink"
]
},
{
"name": "UnifiedSearchContract",
"file": "app/plugins/builtins/unified_search/contracts.py",
"attributes": [
"contract_name",
"generate_embedding",
"hybrid_search",
"find_similar_all_types",
"get_search_registry",
"llm_analyze_query",
"BaseSearchProvider"
]
},
{
"name": "GraphRagContract",
"file": "app/plugins/builtins/graph_rag/contracts.py",
"attributes": [
"contract_name",
"GraphRAGSearchProvider"
]
},
{
"name": "ReportGeneratorContract",
"file": "app/plugins/builtins/report_generator/contracts.py",
"attributes": [
"contract_name",
"ReportTemplate",
"ReportInstance",
"generate_pdf",
"generate_print_pdf",
"generate_preset_report",
"generate_pdf_from_template_content",
"render_template_file",
"render_template_string",
"get_preset_list",
"PRESET_META",
"PRESET_TEMPLATES"
]
},
{
"name": "SystemNotifContract",
"file": "app/plugins/builtins/system_notif/contracts.py",
"attributes": [
"contract_name",
"SystemParticipantHandler"
]
},
{
"name": "EntityLinksContract",
"file": "app/plugins/builtins/entity_links/contracts.py",
"attributes": [
"contract_name",
"EntityLink"
]
},
{
"name": "KommunikationContract",
"file": "app/plugins/builtins/kommunikation/contracts.py",
"attributes": [
"contract_name",
"parse_mentions",
"get_conversation",
"get_messages",
"send_message",
"create_plugin_room",
"get_participant_registry",
"ParticipantHandler",
"MiniAppRegistry",
"MiniAppDef",
"get_miniapp_registry",
"reset_miniapp_registry",
"post_system_message",
"CommConversation",
"CommMessage",
"CommParticipant"
]
},
{
"name": "AiProactiveContract",
"file": "app/plugins/builtins/ai_proactive/contracts.py",
"attributes": [
"contract_name",
"ProactiveSuggestion",
"ContextLog",
"ProactiveSettings",
"handle_context_change",
"get_active_suggestions",
"get_sse_queue",
"get_stats",
"get_user_settings",
"mark_dismissed",
"push_suggestion",
"register_context_tools",
"deep_analysis",
"heartbeat"
]
},
{
"name": "AIAssistantContract",
"file": "app/plugins/builtins/ai_assistant/contracts.py",
"attributes": [
"contract_name",
"get_tool_registry",
"ToolRegistry",
"AITool",
"get_default_provider"
]
},
{
"name": "TagsContract",
"file": "app/plugins/builtins/tags/contracts.py",
"attributes": [
"contract_name",
"Tag",
"TagAssignment"
]
},
{
"name": "PermissionsContract",
"file": "app/plugins/builtins/permissions/contracts.py",
"attributes": [
"contract_name",
"Permission",
"ShareLink"
]
},
{
"name": "McpServerContract",
"file": "app/plugins/builtins/mcp_server/contracts.py",
"attributes": [
"contract_name",
"ToolDefinitions",
"ToolHandlers",
"get_tool_definition",
"get_all_tool_names",
"McpToolDefinition",
"McpToolExecuteRequest",
"McpToolExecuteResponse",
"McpToolListResponse",
"McpToolParameter",
"McpServerConfig"
]
},
{
"name": "AiUiControlContract",
"file": "app/plugins/builtins/ai_ui_control/contracts.py",
"attributes": [
"contract_name",
"AIUIControlWSManager",
"UICommand",
"UICommandCreate",
"UICommandFeedback",
"UICommandResponse",
"UICommandStatus",
"UICommandStatusResponse",
"UICommandType"
]
}
],
"accesses": [
{
"contract": "AIAssistantContract",
"attribute": "get_tool_registry",
"file": "app/plugins/builtins/automation/agent_coordinator.py"
},
{
"contract": "AIAssistantContract",
"attribute": "get_tool_registry",
"file": "app/plugins/builtins/automation/agent_coordinator.py"
},
{
"contract": "GraphRagContract",
"attribute": "GraphRAGSearchProvider",
"file": "app/plugins/builtins/unified_search/provider_registry.py"
},
{
"contract": "AutomationContract",
"attribute": "AgentRun",
"file": "app/plugins/builtins/ai_assistant/external_api.py"
},
{
"contract": "AutomationContract",
"attribute": "AgentRun",
"file": "app/plugins/builtins/ai_assistant/external_api.py"
},
{
"contract": "AutomationContract",
"attribute": "AgentRun",
"file": "app/plugins/builtins/ai_assistant/external_api.py"
},
{
"contract": "DmsContract",
"attribute": "DmsFile",
"file": "app/plugins/builtins/permissions/public_routes.py"
}
],
"mismatches": []
}
@@ -0,0 +1,26 @@
{
"py_defs": 1616,
"py_dead": 629,
"ts_defs": 959,
"ts_dead": 442,
"critical_dead": [
{
"name": "seed_default_workspace",
"file": "app/services/workspace_service.py",
"line": 560,
"lang": "py"
},
{
"name": "register_default_history_hooks",
"file": "app/core/history_hooks.py",
"line": 128,
"lang": "py"
},
{
"name": "register_default_entities",
"file": "app/core/restore_registry.py",
"line": 114,
"lang": "py"
}
]
}
@@ -0,0 +1,33 @@
{
"total_imports": 2274,
"broken": [
{
"file": "app/routes/entity_permissions.py",
"module": "app.models.entity_permission",
"name": "ENTITY_MODELS",
"line": 105,
"issue": "Name 'ENTITY_MODELS' not found in module"
},
{
"file": "app/core/auth.py",
"module": "app.models.session",
"name": "SessionModel",
"line": 261,
"issue": "Name 'SessionModel' not found in module"
},
{
"file": "app/plugins/builtins/automation/agent_comm.py",
"module": "app.plugins.builtins.kommunikation.contracts",
"name": "Room",
"line": 58,
"issue": "Name 'Room' not found in module"
},
{
"file": "app/plugins/builtins/automation/agent_runner.py",
"module": "app.core.cache",
"name": "get_cached_mail_summary",
"line": 115,
"issue": "Name 'get_cached_mail_summary' not found in module"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,701 @@
{
"stores": {
"calendarStore": [
"setVisibleWeek",
"toggleCalendarVisibility",
"goToNextDay",
"goToToday",
"setVisibleMonth",
"setActiveCalendarId",
"goToPrevDay",
"goToPrevWeek",
"setRangeEnd",
"visibleMonth",
"rangeStart",
"visibleWeek",
"selectedEntry",
"activeCalendarId",
"rangeEnd",
"setVisibleDay",
"viewMode",
"goToNextMonth",
"visibleDay",
"date",
"setViewMode",
"entry",
"visibleCalendarIds",
"setRangeStart",
"mode",
"goToPrevMonth",
"setSelectedEntry",
"calendars",
"goToNextWeek",
"setCalendars"
],
"aiUIControlStore": [
"setAIActive",
"value",
"section",
"entity",
"pendingSettings",
"settings",
"clearPending",
"setActiveCommand",
"status",
"activeTab",
"pendingFilter",
"setConnected",
"activeModal",
"lastFeedback",
"connected",
"setLastFeedback",
"feedback",
"command_id",
"setPendingSettings",
"commandHistory",
"setActiveModal",
"active",
"addCommandToHistory",
"activeCommand",
"action",
"filter",
"setPendingFilter",
"aiActive",
"setActiveTab",
"modal"
],
"onboardingStore": [
"completed",
"isActive",
"goToStep",
"prev",
"start",
"skipped",
"complete",
"reset",
"next",
"skip",
"data",
"step"
],
"windowStore": [
"windows",
"minimizeWindow",
"toggleAiChat",
"nextZIndex",
"openWindow",
"const",
"restoreWindow",
"updateWindowSize",
"height",
"newWindow",
"zIndex",
"aiChatVisible",
"config",
"type",
"title",
"position",
"componentProps",
"setActiveWindow",
"updateWindowPosition",
"closeWindow",
"activeWindowId",
"null",
"component",
"size",
"toggleFullscreen",
"width"
],
"commandPaletteStore": [
"close",
"toggle",
"isOpen",
"open"
],
"pluginToolbarStore": [
"updateItem",
"value",
"setActivePlugin",
"label",
"registerItems",
"plugin",
"unregisterPlugin",
"updates",
"query",
"activePlugin",
"null",
"items",
"onClick"
],
"commStore": [
"file_type",
"sort_order",
"locked_by",
"is_direct",
"conversation_id",
"file_size",
"sender_id",
"content",
"participants",
"addMessage",
"role",
"setActiveConversation",
"setTyping",
"setConversations",
"created_at",
"participant_id",
"typingUsers",
"userIds",
"file_name",
"reply_to_id",
"participant_type",
"setMessages",
"conv",
"title",
"blocks",
"edited_at",
"activeConversationId",
"sender_type",
"file_id",
"last_msg_at",
"updateConversation",
"msgs",
"display_name",
"count",
"is_archived",
"reactions",
"loading",
"setLoading",
"created_by_type",
"attachments",
"created_by",
"last_msg_preview",
"setUnread",
"block_data",
"file_source",
"convId",
"is_pinned",
"messages",
"convs",
"content_format",
"unread_count",
"conversations",
"thumbnail_path",
"is_locked",
"last_msg_sender_type",
"block_type",
"unreadCounts",
"metadata"
],
"pluginStore": [
"permission",
"is_core",
"getDetailTabsForEntity",
"default_value",
"entity",
"setManifests",
"loaded",
"badge_key",
"error",
"entityType",
"parent",
"row_span",
"custom_fields",
"detail_tabs",
"dashboard_widgets",
"col_span",
"reset",
"order",
"getCustomFieldsForEntity",
"getAllDashboardWidgets",
"display_name",
"protected",
"version",
"label",
"required",
"path",
"getAllMenuItems",
"loading",
"getAllSettingsPages",
"setLoading",
"field_type",
"group",
"options",
"component",
"page_routes",
"entity_type",
"manifests",
"label_key",
"settings_pages",
"getAllPageRoutes",
"menu_items",
"icon",
"setError"
],
"themeStore": [
"borderRadius",
"applyTheme",
"toggleDarkMode",
"config",
"loadFromStorage",
"accentColor",
"DEFAULT_THEME",
"primaryColor",
"target",
"darkMode",
"amount",
"result",
"setTheme",
"saveToStorage",
"base",
"fontFamily",
"scales"
],
"uiStore": [
"message",
"suggestionSidebarOpen",
"setAISidebarCollapsed",
"openAISidebarProactive",
"locale",
"theme",
"clearToasts",
"toggleAISidebar",
"setLocale",
"open",
"type",
"setTheme",
"collapsed",
"toggleSidebar",
"setAISidebarTab",
"aiSidebarCollapsed",
"clearNotifications",
"setSidebarOpen",
"setMessageSidebarCollapsed",
"removeToast",
"notifications",
"toggleSuggestionSidebar",
"toggleMessageSidebar",
"toasts",
"messageSidebarCollapsed",
"toast",
"removeNotification",
"index",
"addToast",
"sidebarOpen",
"aiSidebarTab"
],
"workspaceStore": [
"is_visible",
"widgets",
"context",
"height",
"config",
"modules",
"visibleModuleKeys",
"workspaces",
"workspace_id",
"setActiveWorkspace",
"menu_order",
"position_x",
"is_active",
"reset",
"description",
"isModuleVisible",
"position_y",
"widget_key",
"isLoading",
"activeWorkspaceId",
"moduleKey",
"loading",
"setLoading",
"setMyWorkspaces",
"module_key",
"setContext",
"is_default",
"myWorkspaces",
"width",
"hasWorkspaces",
"icon"
],
"authStore": [
"currentTenant",
"setUser",
"setAuthenticated",
"first_name",
"is_system_admin",
"role",
"isAuthenticated",
"field_permissions",
"error",
"setError",
"perms",
"fieldPerms",
"isLoading",
"tenant",
"loading",
"setLoading",
"slug",
"permissions",
"last_name",
"user",
"isSystemAdmin",
"authed",
"setPermissions",
"avatar_url",
"tenants",
"setTenant",
"email",
"logout"
]
},
"usage": {},
"unused": {
"calendarStore": [
"setVisibleWeek",
"toggleCalendarVisibility",
"goToNextDay",
"goToToday",
"setVisibleMonth",
"setActiveCalendarId",
"goToPrevDay",
"goToPrevWeek",
"setRangeEnd",
"visibleMonth",
"rangeStart",
"visibleWeek",
"selectedEntry",
"activeCalendarId",
"rangeEnd",
"setVisibleDay",
"viewMode",
"goToNextMonth",
"visibleDay",
"date",
"setViewMode",
"entry",
"visibleCalendarIds",
"setRangeStart",
"mode",
"goToPrevMonth",
"setSelectedEntry",
"calendars",
"goToNextWeek",
"setCalendars"
],
"aiUIControlStore": [
"setAIActive",
"value",
"section",
"entity",
"pendingSettings",
"settings",
"clearPending",
"setActiveCommand",
"status",
"activeTab",
"pendingFilter",
"setConnected",
"activeModal",
"lastFeedback",
"connected",
"setLastFeedback",
"feedback",
"command_id",
"setPendingSettings",
"commandHistory",
"setActiveModal",
"active",
"addCommandToHistory",
"activeCommand",
"action",
"filter",
"setPendingFilter",
"aiActive",
"setActiveTab",
"modal"
],
"onboardingStore": [
"completed",
"isActive",
"goToStep",
"prev",
"start",
"skipped",
"complete",
"reset",
"next",
"skip",
"data",
"step"
],
"windowStore": [
"windows",
"minimizeWindow",
"toggleAiChat",
"nextZIndex",
"openWindow",
"const",
"restoreWindow",
"updateWindowSize",
"height",
"newWindow",
"zIndex",
"aiChatVisible",
"config",
"type",
"title",
"position",
"componentProps",
"setActiveWindow",
"updateWindowPosition",
"closeWindow",
"activeWindowId",
"null",
"component",
"size",
"toggleFullscreen",
"width"
],
"commandPaletteStore": [
"close",
"toggle",
"isOpen",
"open"
],
"pluginToolbarStore": [
"updateItem",
"value",
"setActivePlugin",
"label",
"registerItems",
"plugin",
"unregisterPlugin",
"updates",
"query",
"activePlugin",
"null",
"items",
"onClick"
],
"commStore": [
"file_type",
"sort_order",
"locked_by",
"is_direct",
"conversation_id",
"file_size",
"sender_id",
"content",
"participants",
"addMessage",
"role",
"setActiveConversation",
"setTyping",
"setConversations",
"created_at",
"participant_id",
"typingUsers",
"userIds",
"file_name",
"reply_to_id",
"participant_type",
"setMessages",
"conv",
"title",
"blocks",
"edited_at",
"activeConversationId",
"sender_type",
"file_id",
"last_msg_at",
"updateConversation",
"msgs",
"display_name",
"count",
"is_archived",
"reactions",
"loading",
"setLoading",
"created_by_type",
"attachments",
"created_by",
"last_msg_preview",
"setUnread",
"block_data",
"file_source",
"convId",
"is_pinned",
"messages",
"convs",
"content_format",
"unread_count",
"conversations",
"thumbnail_path",
"is_locked",
"last_msg_sender_type",
"block_type",
"unreadCounts",
"metadata"
],
"pluginStore": [
"permission",
"is_core",
"getDetailTabsForEntity",
"default_value",
"entity",
"setManifests",
"loaded",
"badge_key",
"error",
"entityType",
"parent",
"row_span",
"custom_fields",
"detail_tabs",
"dashboard_widgets",
"col_span",
"reset",
"order",
"getCustomFieldsForEntity",
"getAllDashboardWidgets",
"display_name",
"protected",
"version",
"label",
"required",
"path",
"getAllMenuItems",
"loading",
"getAllSettingsPages",
"setLoading",
"field_type",
"group",
"options",
"component",
"page_routes",
"entity_type",
"manifests",
"label_key",
"settings_pages",
"getAllPageRoutes",
"menu_items",
"icon",
"setError"
],
"themeStore": [
"borderRadius",
"applyTheme",
"toggleDarkMode",
"config",
"loadFromStorage",
"accentColor",
"DEFAULT_THEME",
"primaryColor",
"target",
"darkMode",
"amount",
"result",
"setTheme",
"saveToStorage",
"base",
"fontFamily",
"scales"
],
"uiStore": [
"message",
"suggestionSidebarOpen",
"setAISidebarCollapsed",
"openAISidebarProactive",
"locale",
"theme",
"clearToasts",
"toggleAISidebar",
"setLocale",
"open",
"type",
"setTheme",
"collapsed",
"toggleSidebar",
"setAISidebarTab",
"aiSidebarCollapsed",
"clearNotifications",
"setSidebarOpen",
"setMessageSidebarCollapsed",
"removeToast",
"notifications",
"toggleSuggestionSidebar",
"toggleMessageSidebar",
"toasts",
"messageSidebarCollapsed",
"toast",
"removeNotification",
"index",
"addToast",
"sidebarOpen",
"aiSidebarTab"
],
"workspaceStore": [
"is_visible",
"widgets",
"context",
"height",
"config",
"modules",
"visibleModuleKeys",
"workspaces",
"workspace_id",
"setActiveWorkspace",
"menu_order",
"position_x",
"is_active",
"reset",
"description",
"isModuleVisible",
"position_y",
"widget_key",
"isLoading",
"activeWorkspaceId",
"moduleKey",
"loading",
"setLoading",
"setMyWorkspaces",
"module_key",
"setContext",
"is_default",
"myWorkspaces",
"width",
"hasWorkspaces",
"icon"
],
"authStore": [
"currentTenant",
"setUser",
"setAuthenticated",
"first_name",
"is_system_admin",
"role",
"isAuthenticated",
"field_permissions",
"error",
"setError",
"perms",
"fieldPerms",
"isLoading",
"tenant",
"loading",
"setLoading",
"slug",
"permissions",
"last_name",
"user",
"isSystemAdmin",
"authed",
"setPermissions",
"avatar_url",
"tenants",
"setTenant",
"email",
"logout"
]
}
}
+347
View File
@@ -0,0 +1,347 @@
#!/usr/bin/env python3
"""Trace API Contracts — Find Frontend↔Backend Verkabelungsfehler.
Compares every frontend API call with the backend response fields.
Mismatches = Verkabelungsfehler.
Usage: python3 scripts/test_suite/trace_api_contracts.py
"""
from __future__ import annotations
import ast
import json
import os
import re
import sys
from collections import defaultdict
from pathlib import Path
# Project root
ROOT = Path(__file__).resolve().parent.parent.parent
FRONTEND_SRC = ROOT / "frontend" / "src"
BACKEND_APP = ROOT / "app"
def find_frontend_api_calls() -> list[dict]:
"""Find all API calls in frontend source."""
calls = []
api_dir = FRONTEND_SRC / "api"
if not api_dir.exists():
return calls
for ts_file in api_dir.rglob("*.ts"):
try:
content = ts_file.read_text(encoding="utf-8")
except Exception:
continue
rel_path = str(ts_file.relative_to(FRONTEND_SRC))
# Find apiGet/apiPost/apiPut/apiPatch/apiDelete calls with URL patterns
patterns = [
r"apiGet(?:<[^>]+>)?\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
r"apiPost(?:<[^>]+>)?\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
r"apiPut(?:<[^>]+>)?\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
r"apiPatch(?:<[^>]+>)?\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
r"apiDelete(?:<[^>]+>)?\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
r"\bM\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
r"\bj\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
r"\bSt\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
r"\byt\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
r"\bxt\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
]
for pattern in patterns:
for match in re.finditer(pattern, content):
url = match.group(1)
# Normalize URL — remove template literals, query params
url_clean = re.sub(r"\$\{[^}]+\}", "{param}", url)
url_clean = url_clean.split("?")[0].split("#")[0]
# Determine method from function name
method = "GET"
if "Post" in pattern or "\bj\s*" in pattern:
method = "POST"
elif "Put" in pattern:
method = "PUT"
elif "Patch" in pattern or "\byt\s*" in pattern:
method = "PATCH"
elif "Delete" in pattern or "\bxt\s*" in pattern:
method = "DELETE"
calls.append({
"file": rel_path,
"method": method,
"url": url_clean,
"raw_url": url,
})
return calls
def find_frontend_expected_fields() -> dict[str, list[str]]:
"""Find TypeScript interfaces/types that define expected API response fields."""
fields_by_url = defaultdict(list)
api_dir = FRONTEND_SRC / "api"
if not api_dir.exists():
return fields_by_url
for ts_file in api_dir.rglob("*.ts"):
try:
content = ts_file.read_text(encoding="utf-8")
except Exception:
continue
# Find interface definitions
for match in re.finditer(r"interface\s+(\w+)\s*\{([^}]+)\}", content):
iface_name = match.group(1)
body = match.group(2)
field_names = re.findall(r"(\w+)\s*[?:]", body)
fields_by_url[iface_name] = field_names
return fields_by_url
def find_backend_response_fields() -> list[dict]:
"""Find backend response fields from routes."""
responses = []
routes_dir = BACKEND_APP / "routes"
if not routes_dir.exists():
return responses
for py_file in routes_dir.rglob("*.py"):
try:
content = py_file.read_text(encoding="utf-8")
except Exception:
continue
rel_path = str(py_file.relative_to(BACKEND_APP))
# Find response_model definitions
for match in re.finditer(r"response_model\s*=\s*(\w+)", content):
responses.append({
"file": rel_path,
"response_model": match.group(1),
})
# Find content= dict definitions (inline responses)
for match in re.finditer(r"content\s*=\s*\{([^}]+)\}", content, re.DOTALL):
body = match.group(1)
fields = re.findall(r"['\"](\w+)['\"]\s*:", body)
if fields:
responses.append({
"file": rel_path,
"fields": fields,
"type": "inline",
})
# Find return dict patterns
for match in re.finditer(r"return\s*\{([^}]+)\}", content, re.DOTALL):
body = match.group(1)
fields = re.findall(r"['\"](\w+)['\"]\s*:", body)
if fields:
responses.append({
"file": rel_path,
"fields": fields,
"type": "return_dict",
})
return responses
def find_backend_endpoints() -> list[dict]:
"""Find all backend endpoints from route decorators."""
endpoints = []
routes_dir = BACKEND_APP / "routes"
if not routes_dir.exists():
return endpoints
for py_file in routes_dir.rglob("*.py"):
try:
content = py_file.read_text(encoding="utf-8")
except Exception:
continue
rel_path = str(py_file.relative_to(BACKEND_APP))
# Find router decorators
for match in re.finditer(r"@router\.(get|post|put|patch|delete)\s*\(\s*[\"']([^\"']+)[\"']", content):
method = match.group(1).upper()
path = match.group(2)
endpoints.append({
"file": rel_path,
"method": method,
"path": path,
})
# Also find plugin routes
plugins_dir = BACKEND_APP / "plugins" / "builtins"
if plugins_dir.exists():
for py_file in plugins_dir.rglob("*.py"):
try:
content = py_file.read_text(encoding="utf-8")
except Exception:
continue
rel_path = str(py_file.relative_to(BACKEND_APP))
for match in re.finditer(r"@router\.(get|post|put|patch|delete)\s*\(\s*[\"']([^\"']+)[\"']", content):
method = match.group(1).upper()
path = match.group(2)
endpoints.append({
"file": rel_path,
"method": method,
"path": path,
"is_plugin": True,
})
return endpoints
def normalize_path(path: str) -> str:
"""Normalize a path for comparison."""
# Remove leading /api/v1 if present
path = re.sub(r"^/api/v1/", "/", path)
# Replace {param} patterns
path = re.sub(r"\{[^}]+\}", "{param}", path)
# Remove trailing slash
path = path.rstrip("/")
return path
def check_frontend_calls_have_backend(frontend_calls, backend_endpoints) -> list[dict]:
"""Check if every frontend API call has a matching backend endpoint."""
backend_paths = set()
for ep in backend_endpoints:
normalized = normalize_path(ep["path"])
backend_paths.add((ep["method"], normalized))
mismatches = []
for call in frontend_calls:
url = call["url"]
# Remove /api/v1 prefix if present
url_normalized = normalize_path(url)
key = (call["method"], url_normalized)
if key not in backend_paths:
# Try without method (some endpoints might have different method mapping)
found = False
for bm, bp in backend_paths:
if bp == url_normalized:
found = True
break
if not found:
mismatches.append({
"type": "frontend_call_no_backend",
"method": call["method"],
"url": url,
"file": call["file"],
"severity": "HIGH",
"message": f"Frontend calls {call['method']} {url} but no matching backend endpoint found",
})
return mismatches
def check_backend_endpoints_have_frontend(frontend_calls, backend_endpoints) -> list[dict]:
"""Check if every backend endpoint is called by the frontend."""
frontend_paths = set()
for call in frontend_calls:
url_normalized = normalize_path(call["url"])
frontend_paths.add((call["method"], url_normalized))
mismatches = []
for ep in backend_endpoints:
normalized = normalize_path(ep["path"])
key = (ep["method"], normalized)
if key not in frontend_paths:
mismatches.append({
"type": "backend_endpoint_no_frontend",
"method": ep["method"],
"path": ep["path"],
"file": ep["file"],
"severity": "LOW",
"message": f"Backend endpoint {ep['method']} {ep['path']} is never called by frontend",
})
return mismatches
def main():
print("=" * 70)
print("TRACE API CONTRACTS — Frontend ↔ Backend Verkabelungsfehler")
print("=" * 70)
print("\n[1] Scanning frontend API calls...")
frontend_calls = find_frontend_api_calls()
print(f" Found {len(frontend_calls)} API calls in frontend")
print("\n[2] Scanning backend endpoints...")
backend_endpoints = find_backend_endpoints()
print(f" Found {len(backend_endpoints)} endpoints in backend")
print("\n[3] Scanning backend response fields...")
backend_responses = find_backend_response_fields()
print(f" Found {len(backend_responses)} response definitions")
print("\n[4] Scanning frontend expected fields...")
frontend_fields = find_frontend_expected_fields()
print(f" Found {len(frontend_fields)} TypeScript interfaces")
print("\n[5] Checking frontend calls have matching backend...")
missing_backend = check_frontend_calls_have_backend(frontend_calls, backend_endpoints)
print(f" Found {len(missing_backend)} frontend calls without backend endpoint")
print("\n[6] Checking backend endpoints are called by frontend...")
missing_frontend = check_backend_endpoints_have_frontend(frontend_calls, backend_endpoints)
print(f" Found {len(missing_frontend)} backend endpoints never called by frontend")
# Summary
all_issues = missing_backend + missing_frontend
print("\n" + "=" * 70)
print("SUMMARY")
print("=" * 70)
print(f"Frontend API calls: {len(frontend_calls)}")
print(f"Backend endpoints: {len(backend_endpoints)}")
print(f"Response definitions: {len(backend_responses)}")
print(f"TS interfaces: {len(frontend_fields)}")
print(f"\nISSUES FOUND: {len(all_issues)}")
print(f" HIGH (frontend calls missing backend): {len(missing_backend)}")
print(f" LOW (backend endpoints not called): {len(missing_frontend)}")
if missing_backend:
print("\n--- HIGH SEVERITY: Frontend calls without backend ---")
for m in missing_backend[:30]:
print(f" [{m['method']}] {m['url']} (in {m['file']})")
if len(missing_backend) > 30:
print(f" ... and {len(missing_backend) - 30} more")
if missing_frontend:
print("\n--- LOW SEVERITY: Backend endpoints never called by frontend ---")
for m in missing_frontend[:30]:
print(f" [{m['method']}] {m['path']} (in {m['file']})")
if len(missing_frontend) > 30:
print(f" ... and {len(missing_frontend) - 30} more")
# Write results to JSON
results_file = ROOT / "scripts" / "test_suite" / "results_trace_api_contracts.json"
with open(results_file, "w") as f:
json.dump({
"frontend_calls": frontend_calls,
"backend_endpoints": backend_endpoints,
"issues": all_issues,
"summary": {
"total_frontend_calls": len(frontend_calls),
"total_backend_endpoints": len(backend_endpoints),
"total_issues": len(all_issues),
"high_severity": len(missing_backend),
"low_severity": len(missing_frontend),
},
}, f, indent=2, default=str)
print(f"\nResults written to {results_file}")
return len(all_issues)
if __name__ == "__main__":
sys.exit(main())
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Trace Contracts — Find Contract attribute mismatches (like GraphRagContract)."""
from __future__ import annotations
import ast, re, sys, json
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
APP = ROOT / "app"
def find_contracts() -> list[dict]:
contracts = []
for py in APP.rglob("*.py"):
try: content = py.read_text(); tree = ast.parse(content)
except: continue
rel = str(py.relative_to(ROOT))
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
if "Contract" in node.name or node.name.endswith("Contract"):
attrs = []
for item in node.body:
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
attrs.append(item.target.id)
elif isinstance(item, ast.Assign):
for t in item.targets:
if isinstance(t, ast.Name): attrs.append(t.id)
contracts.append({"name": node.name, "file": rel, "attributes": attrs})
return contracts
def find_contract_access() -> list[dict]:
accesses = []
for py in APP.rglob("*.py"):
try: content = py.read_text()
except: continue
rel = str(py.relative_to(ROOT))
for m in re.finditer(r'(\w+Contract)\.(\w+)', content):
accesses.append({"contract": m.group(1), "attribute": m.group(2), "file": rel})
return accesses
def main():
print("=" * 70)
print("TRACE CONTRACTS — Contract Attribute Mismatches")
print("=" * 70)
contracts = find_contracts()
accesses = find_contract_access()
contract_attrs = {c["name"]: set(c["attributes"]) for c in contracts}
mismatches = []
for acc in accesses:
cname = acc["contract"]
attr = acc["attribute"]
if cname in contract_attrs and attr not in contract_attrs[cname]:
mismatches.append(acc)
print(f"\nContracts found: {len(contracts)}")
print(f"Contract accesses: {len(accesses)}")
print(f"\nMISMATCHES: {len(mismatches)}")
if mismatches:
print("\n--- Contract attribute mismatches ---")
for m in mismatches: print(f" {m['contract']}.{m['attribute']} (in {m['file']})")
results = {"contracts": contracts, "accesses": accesses, "mismatches": mismatches}
with open(ROOT / "scripts" / "test_suite" / "results_trace_contracts.json", "w") as f:
json.dump(results, f, indent=2, default=str)
return len(mismatches)
if __name__ == "__main__": sys.exit(main())
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Trace Functions — Find dead functions (defined but never called)."""
from __future__ import annotations
import ast, re, sys, json
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
APP = ROOT / "app"
FRONTEND = ROOT / "frontend" / "src"
# Critical functions that MUST be called
CRITICAL_FUNCTIONS = [
"seed_admin", "seed_default_workspace", "sync_plugin_schema",
"init_permission_registry", "register_default_entities", "register_default_history_hooks",
"discover_builtins", "load_tmp_chats", "initialize_agent",
]
def find_function_defs_py() -> list[dict]:
defs = []
for py in APP.rglob("*.py"):
try: content = py.read_text(); tree = ast.parse(content)
except: continue
rel = str(py.relative_to(ROOT))
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if not node.name.startswith("_") and len(node.name) > 3:
defs.append({"name": node.name, "file": rel, "line": node.lineno, "lang": "py"})
return defs
def find_function_calls_py() -> set[str]:
calls = set()
for py in APP.rglob("*.py"):
try: content = py.read_text(); tree = ast.parse(content)
except: continue
for node in ast.walk(tree):
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name):
calls.add(node.func.id)
elif isinstance(node.func, ast.Attribute):
calls.add(node.func.attr)
return calls
def find_function_defs_ts() -> list[dict]:
defs = []
for ts in FRONTEND.rglob("*.ts"):
try: content = ts.read_text()
except: continue
rel = str(ts.relative_to(ROOT))
for m in re.finditer(r'(?:export\s+)?(?:async\s+)?function\s+(\w+)', content):
name = m.group(1)
if not name.startswith("_") and len(name) > 3:
defs.append({"name": name, "file": rel, "lang": "ts"})
for m in re.finditer(r'(?:export\s+)?const\s+(\w+)\s*=', content):
name = m.group(1)
if not name.startswith("_") and len(name) > 3:
defs.append({"name": name, "file": rel, "lang": "ts"})
return defs
def find_function_calls_ts() -> set[str]:
calls = set()
for ts in FRONTEND.rglob("*.ts"):
try: content = ts.read_text()
except: continue
# Find function calls: word followed by (
for m in re.finditer(r'\b(\w+)\s*\(', content):
calls.add(m.group(1))
return calls
def main():
print("=" * 70)
print("TRACE FUNCTIONS — Dead Functions (defined but never called)")
print("=" * 70)
py_defs = find_function_defs_py()
py_calls = find_function_calls_py()
ts_defs = find_function_defs_ts()
ts_calls = find_function_calls_ts()
py_dead = [d for d in py_defs if d["name"] not in py_calls]
ts_dead = [d for d in ts_defs if d["name"] not in ts_calls]
critical_dead = [d for d in py_dead if d["name"] in CRITICAL_FUNCTIONS]
print(f"\nPython functions defined: {len(py_defs)}")
print(f"Python functions called: {len(py_calls)}")
print(f"Python dead functions: {len(py_dead)}")
print(f"TS functions defined: {len(ts_defs)}")
print(f"TS functions called: {len(ts_calls)}")
print(f"TS dead functions: {len(ts_dead)}")
print(f"\nCRITICAL dead functions: {len(critical_dead)}")
if critical_dead:
print("\n--- CRITICAL: Functions that MUST be called but aren't ---")
for d in critical_dead: print(f" {d['name']} (in {d['file']}:{d.get('line','?')})")
results = {"py_defs": len(py_defs), "py_dead": len(py_dead), "ts_defs": len(ts_defs), "ts_dead": len(ts_dead), "critical_dead": critical_dead}
with open(ROOT / "scripts" / "test_suite" / "results_trace_functions.json", "w") as f:
json.dump(results, f, indent=2, default=str)
return len(critical_dead)
if __name__ == "__main__": sys.exit(main())
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Trace Hooks — Find orphan hooks (registered but never triggered) and orphan triggers (triggered but never received)."""
from __future__ import annotations
import re, sys, json
from pathlib import Path
from collections import defaultdict
ROOT = Path(__file__).resolve().parent.parent.parent
APP = ROOT / "app"
def find_hook_registrations() -> list[dict]:
regs = []
for py in APP.rglob("*.py"):
try: content = py.read_text()
except: continue
rel = str(py.relative_to(ROOT))
for m in re.finditer(r'register_(?:action|filter)s?\s*\(\s*["\']([^"\']+)['"]', content):
regs.append({"file": rel, "hook_type": "action_or_filter", "hook_name": m.group(1)})
for m in re.finditer(r'register_actions_by_owner\s*\(\s*["\']([^"\']+)['"]', content):
regs.append({"file": rel, "hook_type": "action_group", "hook_name": m.group(1)})
return regs
def find_hook_triggers() -> list[dict]:
triggers = []
for py in APP.rglob("*.py"):
try: content = py.read_text()
except: continue
rel = str(py.relative_to(ROOT))
for m in re.finditer(r'do_action\s*\(\s*["\']([^"\']+)['"]', content):
triggers.append({"file": rel, "hook_name": m.group(1), "type": "action"})
for m in re.finditer(r'apply_filters?\s*\(\s*["\']([^"\']+)['"]', content):
triggers.append({"file": rel, "hook_name": m.group(1), "type": "filter"})
for m in re.finditer(r'trigger\s*\(\s*["\']([^"\']+)['"]', content):
triggers.append({"file": rel, "hook_name": m.group(1), "type": "trigger"})
return triggers
def main():
print("=" * 70)
print("TRACE HOOKS — Orphan Hooks & Triggers")
print("=" * 70)
regs = find_hook_registrations()
triggers = find_hook_triggers()
reg_names = {r["hook_name"] for r in regs}
trig_names = {t["hook_name"] for t in triggers}
orphans_reg = [r for r in regs if r["hook_name"] not in trig_names]
orphans_trig = [t for t in triggers if t["hook_name"] not in reg_names]
print(f"\nHook registrations: {len(regs)}")
print(f"Hook triggers: {len(triggers)}")
print(f"\nISSUES: {len(orphans_reg) + len(orphans_trig)}")
print(f" Registered but never triggered: {len(orphans_reg)}")
print(f" Triggered but never received: {len(orphans_trig)}")
if orphans_reg:
print("\n--- Registered but never triggered ---")
for o in orphans_reg[:20]: print(f" {o['hook_name']} (in {o['file']})")
if orphans_trig:
print("\n--- Triggered but never received ---")
for o in orphans_trig[:20]: print(f" {o['hook_name']} (in {o['file']})")
results = {"registrations": regs, "triggers": triggers, "orphan_registrations": orphans_reg, "orphan_triggers": orphans_trig}
with open(ROOT / "scripts" / "test_suite" / "results_trace_hooks.json", "w") as f:
json.dump(results, f, indent=2, default=str)
return len(orphans_reg) + len(orphans_trig)
if __name__ == "__main__": sys.exit(main())
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Trace Imports — Find broken/missing imports in backend."""
from __future__ import annotations
import ast, sys, json
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
APP = ROOT / "app"
def find_imports() -> list[dict]:
imports = []
for py in APP.rglob("*.py"):
try: content = py.read_text(); tree = ast.parse(content)
except: continue
rel = str(py.relative_to(ROOT))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
if node.module and node.module.startswith("app."):
for alias in node.names:
imports.append({"file": rel, "module": node.module, "name": alias.name, "line": node.lineno})
return imports
def check_imports_exist(imports: list[dict]) -> list[dict]:
broken = []
for imp in imports:
module_path = imp["module"].replace(".", "/") + ".py"
full_path = ROOT / module_path
if not full_path.exists():
# Try as package
pkg_path = ROOT / module_path.replace(".py", "/__init__.py")
if not pkg_path.exists():
broken.append({**imp, "issue": "Module not found"})
continue
# Check if name exists in module
try:
content = full_path.read_text()
if imp["name"] not in content and imp["name"] != "__init__":
# Could be imported via __init__.py
if not full_path.name == "__init__.py":
broken.append({**imp, "issue": f"Name '{imp['name']}' not found in module"})
except: pass
return broken
def main():
print("=" * 70)
print("TRACE IMPORTS — Broken/Missing Imports")
print("=" * 70)
imports = find_imports()
broken = check_imports_exist(imports)
print(f"\nTotal imports: {len(imports)}")
print(f"Broken imports: {len(broken)}")
if broken:
print("\n--- Broken imports ---")
for b in broken[:30]: print(f" {b['file']}:{b['line']}{b['module']}.{b['name']}{b['issue']}")
results = {"total_imports": len(imports), "broken": broken}
with open(ROOT / "scripts" / "test_suite" / "results_trace_imports.json", "w") as f:
json.dump(results, f, indent=2, default=str)
return len(broken)
if __name__ == "__main__": sys.exit(main())
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Trace Plugins — Find plugin→manifest→frontend verkabelungsfehler."""
from __future__ import annotations
import ast, re, sys, json
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
APP = ROOT / "app"
FRONTEND = ROOT / "frontend" / "src"
def find_plugin_manifests() -> list[dict]:
plugins = []
builtins = APP / "plugins" / "builtins"
if not builtins.exists(): return plugins
for d in builtins.iterdir():
if not d.is_dir() or d.name.startswith("_"): continue
manifest_file = d / "manifest.py"
plugin_info = {"name": d.name, "path": str(d.relative_to(ROOT)), "has_manifest": manifest_file.exists()}
if manifest_file.exists():
try: content = manifest_file.read_text()
except: continue
for m in re.finditer(r'menu_items\s*[=:]\s*\[', content): plugin_info["has_menu_items"] = True
for m in re.finditer(r'page_routes\s*[=:]\s*\[', content): plugin_info["has_page_routes"] = True
for m in re.finditer(r'settings_pages\s*[=:]\s*\[', content): plugin_info["has_settings_pages"] = True
for m in re.finditer(r'detail_tabs\s*[=:]\s*\[', content): plugin_info["has_detail_tabs"] = True
plugins.append(plugin_info)
return plugins
def find_frontend_plugin_refs() -> dict[str, list[str]]:
refs = {}
for ts in FRONTEND.rglob("*.ts"):
try: content = ts.read_text()
except: continue
for m in re.finditer(r'@/plugins/([\w-]+)', content):
refs.setdefault(m.group(1), []).append(str(ts.relative_to(ROOT)))
return refs
def find_plugin_routes() -> list[dict]:
routes = []
builtins = APP / "plugins" / "builtins"
if not builtins.exists(): return routes
for py in builtins.rglob("*.py"):
try: content = py.read_text()
except: continue
rel = str(py.relative_to(ROOT))
for m in re.finditer(r'@router\.(get|post|put|patch|delete)\s*\(\s*["\']([^"\']+)["\']', content):
routes.append({"method": m.group(1).upper(), "path": m.group(2), "file": rel})
return routes
def main():
print("=" * 70)
print("TRACE PLUGINS — Plugin→Manifest→Frontend Verkabelungsfehler")
print("=" * 70)
plugins = find_plugin_manifests()
frontend_refs = find_frontend_plugin_refs()
plugin_routes = find_plugin_routes()
issues = []
for p in plugins:
if not p["has_manifest"]:
issues.append({"plugin": p["name"], "issue": "No manifest.py", "severity": "HIGH"})
if p.get("has_menu_items") and p["name"] not in frontend_refs:
issues.append({"plugin": p["name"], "issue": "Has menu_items but no frontend reference", "severity": "MEDIUM"})
print(f"\nPlugins found: {len(plugins)}")
print(f"Plugin routes: {len(plugin_routes)}")
print(f"Frontend plugin refs: {len(frontend_refs)}")
print(f"\nISSUES: {len(issues)}")
if issues:
print("\n--- Plugin issues ---")
for i in issues: print(f" [{i['severity']}] {i['plugin']}: {i['issue']}")
results = {"plugins": plugins, "frontend_refs": frontend_refs, "plugin_routes": plugin_routes, "issues": issues}
with open(ROOT / "scripts" / "test_suite" / "results_trace_plugins.json", "w") as f:
json.dump(results, f, indent=2, default=str)
return len(issues)
if __name__ == "__main__": sys.exit(main())
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Trace Stores — Find unused store actions/state in frontend."""
from __future__ import annotations
import re, sys, json
from pathlib import Path
from collections import defaultdict
ROOT = Path(__file__).resolve().parent.parent.parent
FRONTEND = ROOT / "frontend" / "src"
def find_store_actions() -> dict[str, list[str]]:
stores = {}
store_dir = FRONTEND / "store"
if not store_dir.exists(): return stores
for ts in store_dir.glob("*.ts"):
try: content = ts.read_text()
except: continue
store_name = ts.stem
actions = []
# Find action names in zustand store definitions
for m in re.finditer(r'(\w+)\s*:', content):
name = m.group(1)
if name not in ('state', 'set', 'get', 'name', 'storage', 'partialize') and len(name) > 3:
actions.append(name)
stores[store_name] = list(set(actions))
return stores
def find_store_usage() -> dict[str, set[str]]:
usage = defaultdict(set)
for ts in FRONTEND.rglob("*.ts"):
try: content = ts.read_text()
except: continue
for store_name in ['authStore', 'uiStore', 'themeStore', 'workspaceStore', 'pluginStore', 'commStore', 'calendarStore', 'commandPaletteStore', 'onboardingStore', 'windowStore', 'aiUIControlStore', 'pluginToolbarStore']:
if store_name in content:
# Find which actions/state are accessed
for m in re.finditer(rf'{store_name}\.*(\w+)', content):
usage[store_name].add(m.group(1))
return usage
def main():
print("=" * 70)
print("TRACE STORES — Unused Store Actions/State")
print("=" * 70)
stores = find_store_actions()
usage = find_store_usage()
unused = {}
total_unused = 0
for store_name, actions in stores.items():
used = usage.get(store_name, set())
unused_actions = [a for a in actions if a not in used]
if unused_actions:
unused[store_name] = unused_actions
total_unused += len(unused_actions)
print(f"\nStores found: {len(stores)}")
for name, actions in stores.items():
used = len(usage.get(name, set()))
print(f" {name}: {len(actions)} defined, {used} used, {len(unused.get(name, []))} unused")
print(f"\nTotal unused store actions: {total_unused}")
if unused:
print("\n--- Unused store actions ---")
for store, actions in unused.items():
for a in actions[:10]: print(f" {store}.{a}")
results = {"stores": stores, "usage": {k: list(v) for k, v in usage.items()}, "unused": unused}
with open(ROOT / "scripts" / "test_suite" / "results_trace_stores.json", "w") as f:
json.dump(results, f, indent=2, default=str)
return total_unused
if __name__ == "__main__": sys.exit(main())