fix(imports): 3 broken imports — ENTITY_MODELS path, Room→CommConversation, get_cached_mail_summary→MailService
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -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())
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user