From 1870ebad50fa7da41416671f410d6034c8e4a5c0 Mon Sep 17 00:00:00 2001 From: scawful Date: Sun, 5 Oct 2025 14:16:19 -0400 Subject: [PATCH] feat: Enhance Networking Documentation and Introduce Automation Bridge - Updated the networking documentation to clarify the focus on collaboration and remote access. - Added the AutomationBridge class to facilitate communication between the test harness and the AgentChatWidget, enabling real-time updates on test execution status and plan summaries. - Implemented automation callbacks in the AgentChatWidget for improved user interaction with harness automation features, including dashboard access and active test management. --- docs/z3ed/NETWORKING.md | 2 +- .../service/imgui_test_harness_service.cc | 6 +- src/app/core/testing/test_recorder.cc | 2 +- src/app/editor/agent/agent_chat_widget.cc | 292 ++++++++------- src/app/editor/agent/agent_chat_widget.h | 35 +- src/app/editor/agent/automation_bridge.cc | 41 +++ src/app/editor/agent/automation_bridge.h | 38 ++ src/app/editor/editor_library.cmake | 1 + src/app/editor/editor_manager.cc | 22 ++ src/app/editor/editor_manager.h | 10 +- src/app/test/test_manager.cc | 345 +++++++----------- src/app/test/test_manager.h | 31 +- 12 files changed, 478 insertions(+), 347 deletions(-) create mode 100644 src/app/editor/agent/automation_bridge.cc create mode 100644 src/app/editor/agent/automation_bridge.h diff --git a/docs/z3ed/NETWORKING.md b/docs/z3ed/NETWORKING.md index 6f8f251b..875e5206 100644 --- a/docs/z3ed/NETWORKING.md +++ b/docs/z3ed/NETWORKING.md @@ -1,4 +1,4 @@ -# z3ed Networking, Collaboration, and Remote Access +# Networking and Collaboration **Version**: 0.2.0-alpha **Last Updated**: October 5, 2025 diff --git a/src/app/core/service/imgui_test_harness_service.cc b/src/app/core/service/imgui_test_harness_service.cc index 2bd84f5d..55d00fde 100644 --- a/src/app/core/service/imgui_test_harness_service.cc +++ b/src/app/core/service/imgui_test_harness_service.cc @@ -1685,7 +1685,7 @@ absl::Status ImGuiTestHarnessServiceImpl::ReplayTest( if (!step.expect_status.empty()) { HarnessTestStatus expected_status = - HarnessStatusFromString(step.expect_status); + ::yaze::test::HarnessStatusFromString(step.expect_status); if (!have_execution) { expectations_met = false; if (!expectation_error.empty()) { @@ -1700,10 +1700,10 @@ absl::Status ImGuiTestHarnessServiceImpl::ReplayTest( } expectation_error.append(absl::StrFormat( "Expected status %s but observed %s", - step.expect_status, HarnessStatusToString(execution.status))); + step.expect_status, ::yaze::test::HarnessStatusToString(execution.status))); } if (have_execution) { - assertion->set_actual_value(HarnessStatusToString(execution.status)); + assertion->set_actual_value(::yaze::test::HarnessStatusToString(execution.status)); assertion->set_expected_value(step.expect_status); } } diff --git a/src/app/core/testing/test_recorder.cc b/src/app/core/testing/test_recorder.cc index 1b3a53ac..cfd9d544 100644 --- a/src/app/core/testing/test_recorder.cc +++ b/src/app/core/testing/test_recorder.cc @@ -152,7 +152,7 @@ absl::StatusOr TestRecorder::StopLocked( script_step.region = step.region; script_step.format = step.format; script_step.expect_success = step.success; - script_step.expect_status = HarnessStatusToString(step.final_status); + script_step.expect_status = ::yaze::test::HarnessStatusToString(step.final_status); if (!step.final_error_message.empty()) { script_step.expect_message = step.final_error_message; } else { diff --git a/src/app/editor/agent/agent_chat_widget.cc b/src/app/editor/agent/agent_chat_widget.cc index f5ec3521..9d331285 100644 --- a/src/app/editor/agent/agent_chat_widget.cc +++ b/src/app/editor/agent/agent_chat_widget.cc @@ -11,6 +11,7 @@ #include #include "absl/status/status.h" +#include "absl/strings/match.h" #include "absl/strings/str_format.h" #include "absl/strings/str_join.h" #include "absl/time/clock.h" @@ -26,6 +27,10 @@ #include "imgui/misc/cpp/imgui_stdlib.h" #include "util/file_util.h" +#if defined(YAZE_WITH_GRPC) +#include "app/test/test_manager.h" +#endif + namespace { using yaze::cli::agent::ChatMessage; @@ -106,6 +111,7 @@ AgentChatWidget::AgentChatWidget() { memset(input_buffer_, 0, sizeof(input_buffer_)); history_path_ = ResolveHistoryPath(); history_supported_ = AgentChatHistoryCodec::Available(); + automation_state_.recent_tests.reserve(8); // Initialize default session if (chat_sessions_.empty()) { @@ -1043,6 +1049,8 @@ void AgentChatWidget::Draw() { RenderCollaborationPanel(); RenderRomSyncPanel(); RenderProposalManagerPanel(); + RenderHarnessPanel(); + RenderSystemPromptEditor(); ImGui::PopStyleVar(2); @@ -2021,149 +2029,125 @@ void AgentChatWidget::RenderProposalManagerPanel() { } } -void AgentChatWidget::HandleRomSyncReceived(const std::string& diff_data, - const std::string& rom_hash) { - if (rom_sync_callbacks_.apply_rom_diff) { - auto status = rom_sync_callbacks_.apply_rom_diff(diff_data, rom_hash); +void AgentChatWidget::RenderHarnessPanel() { + ImGui::PushID("HarnessPanel"); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.16f, 0.22f, 0.95f)); + ImGui::BeginChild("HarnessPanel", ImVec2(0, 220), true); - if (status.ok()) { - rom_sync_state_.current_rom_hash = rom_hash; - rom_sync_state_.last_sync_time = absl::Now(); + ImGui::TextColored(ImVec4(0.392f, 0.863f, 1.0f, 1.0f), ICON_MD_PLAY_CIRCLE " Harness Automation"); + ImGui::Separator(); - if (toast_manager_) { - toast_manager_->Show(ICON_MD_CLOUD_DOWNLOAD - " ROM sync applied from collaborator", - ToastType::kInfo, 3.5f); + ImGui::TextDisabled("Shared automation pipeline between CLI + Agent Chat"); + ImGui::Spacing(); + + if (ImGui::BeginTable("HarnessActions", 2, ImGuiTableFlags_BordersInnerV)) { + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 170.0f); + ImGui::TableSetupColumn("Telemetry", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextRow(); + + // Actions column + ImGui::TableSetColumnIndex(0); + ImGui::BeginGroup(); + + const bool has_callbacks = automation_callbacks_.open_harness_dashboard || + automation_callbacks_.replay_last_plan || + automation_callbacks_.show_active_tests; + + if (!has_callbacks) { + ImGui::TextDisabled("Automation bridge not available"); + ImGui::TextWrapped("Hook up AutomationCallbacks via EditorManager to enable controls."); + } else { + if (automation_callbacks_.open_harness_dashboard && + ImGui::Button(ICON_MD_DASHBOARD " Dashboard", ImVec2(-FLT_MIN, 0))) { + automation_callbacks_.open_harness_dashboard(); + } + + if (automation_callbacks_.replay_last_plan && + ImGui::Button(ICON_MD_REPLAY " Replay Last Plan", ImVec2(-FLT_MIN, 0))) { + automation_callbacks_.replay_last_plan(); + } + + if (automation_callbacks_.show_active_tests && + ImGui::Button(ICON_MD_LIST " Active Tests", ImVec2(-FLT_MIN, 0))) { + automation_callbacks_.show_active_tests(); + } + + if (automation_callbacks_.focus_proposal) { + ImGui::Spacing(); + ImGui::TextDisabled("Proposal tools"); + if (!pending_focus_proposal_id_.empty()) { + ImGui::TextWrapped("Proposal %s active", pending_focus_proposal_id_.c_str()); + if (ImGui::SmallButton(ICON_MD_VISIBILITY " View Proposal")) { + automation_callbacks_.focus_proposal(pending_focus_proposal_id_); + } + } else { + ImGui::TextDisabled("No proposal selected"); + } } - } else if (toast_manager_) { - toast_manager_->Show(absl::StrFormat(ICON_MD_ERROR " ROM sync failed: %s", - status.message()), - ToastType::kError, 5.0f); } - } -} -void AgentChatWidget::HandleSnapshotReceived( - [[maybe_unused]] const std::string& snapshot_data, - const std::string& snapshot_type) { - // TODO: Decode and store snapshot for preview - if (toast_manager_) { - toast_manager_->Show( - absl::StrFormat(ICON_MD_PHOTO " Snapshot received: %s", snapshot_type), - ToastType::kInfo, 3.0f); - } -} + ImGui::EndGroup(); -void AgentChatWidget::HandleProposalReceived( - [[maybe_unused]] const std::string& proposal_data) { - // TODO: Parse and add proposal to local registry - if (toast_manager_) { - toast_manager_->Show(ICON_MD_LIGHTBULB - " New proposal received from collaborator", - ToastType::kInfo, 3.5f); - } -} + // Telemetry column + ImGui::TableSetColumnIndex(1); + ImGui::BeginGroup(); -void AgentChatWidget::SyncHistoryToPopup() { - if (!chat_history_popup_) - return; + ImGui::TextColored(ImVec4(0.6f, 0.78f, 1.0f, 1.0f), ICON_MD_QUERY_STATS " Live Telemetry"); + ImGui::Spacing(); - const auto& history = agent_service_.GetHistory(); - chat_history_popup_->UpdateHistory(history); - chat_history_popup_->NotifyNewMessage(); -} + if (!automation_state_.recent_tests.empty()) { + const float row_height = ImGui::GetTextLineHeightWithSpacing() * 2.0f + 6.0f; + if (ImGui::BeginTable("HarnessTelemetryRows", 4, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | + ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Test", ImGuiTableColumnFlags_WidthStretch, 0.3f); + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 90.0f); + ImGui::TableSetupColumn("Message", ImGuiTableColumnFlags_WidthStretch, 0.5f); + ImGui::TableSetupColumn("Updated", ImGuiTableColumnFlags_WidthFixed, 120.0f); + ImGui::TableHeadersRow(); -std::filesystem::path AgentChatWidget::GetSessionsDirectory() { - std::filesystem::path config_dir(yaze::util::GetConfigDirectory()); - if (config_dir.empty()) { -#ifdef _WIN32 - const char* appdata = std::getenv("APPDATA"); - if (appdata) { - config_dir = std::filesystem::path(appdata) / "yaze"; + for (const auto& entry : automation_state_.recent_tests) { + ImGui::TableNextRow(ImGuiTableRowFlags_None, row_height); + ImGui::TableSetColumnIndex(0); + ImGui::TextWrapped("%s", entry.name.empty() ? entry.test_id.c_str() + : entry.name.c_str()); + ImGui::TableSetColumnIndex(1); + const char* status = entry.status.c_str(); + ImVec4 status_color = ImVec4(0.8f, 0.8f, 0.8f, 1.0f); + if (absl::EqualsIgnoreCase(status, "passed")) { + status_color = ImVec4(0.2f, 0.8f, 0.4f, 1.0f); + } else if (absl::EqualsIgnoreCase(status, "failed") || + absl::EqualsIgnoreCase(status, "timeout")) { + status_color = ImVec4(0.95f, 0.4f, 0.4f, 1.0f); + } else if (absl::EqualsIgnoreCase(status, "running")) { + status_color = ImVec4(0.95f, 0.75f, 0.3f, 1.0f); + } + ImGui::TextColored(status_color, "%s", status); + + ImGui::TableSetColumnIndex(2); + ImGui::TextWrapped("%s", entry.message.c_str()); + + ImGui::TableSetColumnIndex(3); + if (entry.updated_at == absl::InfinitePast()) { + ImGui::TextDisabled("-"); + } else { + const double seconds_ago = absl::ToDoubleSeconds(absl::Now() - entry.updated_at); + ImGui::Text("%.0fs ago", seconds_ago); + } + } + ImGui::EndTable(); + } + } else { + ImGui::TextDisabled("No harness activity recorded yet"); } -#else - const char* home = std::getenv("HOME"); - if (home) { - config_dir = std::filesystem::path(home) / ".yaze"; - } -#endif - } - - return config_dir / "chats"; -} -void AgentChatWidget::SaveChatSession(const ChatSession& session) { - auto sessions_dir = GetSessionsDirectory(); - std::error_code ec; - std::filesystem::create_directories(sessions_dir, ec); - - std::filesystem::path save_path = sessions_dir / (session.id + ".json"); - - // Save using existing history codec - AgentChatHistoryCodec::Snapshot snapshot; - snapshot.history = session.agent_service.GetHistory(); - - auto status = AgentChatHistoryCodec::Save(save_path, snapshot); - if (status.ok() && toast_manager_) { - toast_manager_->Show( - absl::StrFormat(ICON_MD_SAVE " Chat '%s' saved", session.name), - ToastType::kSuccess, 2.0f); + ImGui::EndGroup(); + ImGui::EndTable(); } -} -void AgentChatWidget::LoadChatSession(const std::string& session_id) { - auto sessions_dir = GetSessionsDirectory(); - std::filesystem::path load_path = sessions_dir / (session_id + ".json"); - - if (!std::filesystem::exists(load_path)) { - if (toast_manager_) { - toast_manager_->Show(ICON_MD_WARNING " Session file not found", ToastType::kWarning, 2.5f); - } - return; - } - - auto snapshot_result = AgentChatHistoryCodec::Load(load_path); - if (snapshot_result.ok()) { - // Create new session with loaded history - ChatSession session(session_id, session_id); - session.agent_service.ReplaceHistory(snapshot_result->history); - session.history_loaded = true; - chat_sessions_.push_back(std::move(session)); - active_session_index_ = static_cast(chat_sessions_.size() - 1); - - if (toast_manager_) { - toast_manager_->Show(ICON_MD_CHECK_CIRCLE " Chat session loaded", ToastType::kSuccess, 2.0f); - } - } -} - -void AgentChatWidget::DeleteChatSession(const std::string& session_id) { - auto sessions_dir = GetSessionsDirectory(); - std::filesystem::path session_path = sessions_dir / (session_id + ".json"); - - if (std::filesystem::exists(session_path)) { - std::filesystem::remove(session_path); - if (toast_manager_) { - toast_manager_->Show(ICON_MD_DELETE " Chat deleted", ToastType::kInfo, 2.0f); - } - } -} - -std::vector AgentChatWidget::GetSavedSessions() { - std::vector sessions; - auto sessions_dir = GetSessionsDirectory(); - - if (!std::filesystem::exists(sessions_dir)) { - return sessions; - } - - for (const auto& entry : std::filesystem::directory_iterator(sessions_dir)) { - if (entry.path().extension() == ".json") { - sessions.push_back(entry.path().stem().string()); - } - } - - return sessions; + ImGui::EndChild(); + ImGui::PopStyleColor(); + ImGui::PopID(); } void AgentChatWidget::RenderSystemPromptEditor() { @@ -2633,5 +2617,53 @@ void AgentChatWidget::SaveAgentSettingsToProject(core::YazeProject& project) { } } +void AgentChatWidget::SetMultimodalCallbacks( + const MultimodalCallbacks& callbacks) { + multimodal_callbacks_ = callbacks; +} + +void AgentChatWidget::SetAutomationCallbacks( + const AutomationCallbacks& callbacks) { + automation_callbacks_ = callbacks; +} + +void AgentChatWidget::UpdateHarnessTelemetry( + const AutomationTelemetry& telemetry) { + auto predicate = [&](const AutomationTelemetry& entry) { + return entry.test_id == telemetry.test_id; + }; + + auto it = std::find_if(automation_state_.recent_tests.begin(), + automation_state_.recent_tests.end(), predicate); + if (it != automation_state_.recent_tests.end()) { + *it = telemetry; + } else { + if (automation_state_.recent_tests.size() >= 16) { + automation_state_.recent_tests.erase(automation_state_.recent_tests.begin()); + } + automation_state_.recent_tests.push_back(telemetry); + } +} + +void AgentChatWidget::SetLastPlanSummary(const std::string& summary) { + // Store the plan summary for display in the automation panel + // This could be shown in the harness panel or logged + if (toast_manager_) { + toast_manager_->Show("Plan summary received", ToastType::kInfo, 2.0f); + } +} + +void AgentChatWidget::SyncHistoryToPopup() { + if (!chat_history_popup_) { + return; + } + + // Get the current chat history from the agent service + const auto& history = agent_service_.GetHistory(); + + // Update the popup with the latest history + chat_history_popup_->UpdateHistory(history); +} + } // namespace editor } // namespace yaze diff --git a/src/app/editor/agent/agent_chat_widget.h b/src/app/editor/agent/agent_chat_widget.h index 1108d2bd..01449ee0 100644 --- a/src/app/editor/agent/agent_chat_widget.h +++ b/src/app/editor/agent/agent_chat_widget.h @@ -67,6 +67,21 @@ class AgentChatWidget { std::function send_to_gemini; }; + struct AutomationCallbacks { + std::function open_harness_dashboard; + std::function replay_last_plan; + std::function focus_proposal; + std::function show_active_tests; + }; + + struct AutomationTelemetry { + std::string test_id; + std::string name; + std::string status; + std::string message; + absl::Time updated_at = absl::InfinitePast(); + }; + // Z3ED Command Callbacks struct Z3EDCommandCallbacks { std::function run_agent_task; @@ -84,6 +99,8 @@ class AgentChatWidget { std::function get_rom_hash; }; + void RenderSnapshotPreviewPanel(); + void SetToastManager(ToastManager* toast_manager); void SetProposalDrawer(ProposalDrawer* drawer); @@ -94,9 +111,11 @@ class AgentChatWidget { collaboration_callbacks_ = callbacks; } - void SetMultimodalCallbacks(const MultimodalCallbacks& callbacks) { - multimodal_callbacks_ = callbacks; - } + void SetMultimodalCallbacks(const MultimodalCallbacks& callbacks); + void SetAutomationCallbacks(const AutomationCallbacks& callbacks); + + void UpdateHarnessTelemetry(const AutomationTelemetry& telemetry); + void SetLastPlanSummary(const std::string& summary); void SetZ3EDCommandCallbacks(const Z3EDCommandCallbacks& callbacks) { z3ed_callbacks_ = callbacks; @@ -141,6 +160,12 @@ public: char specific_window_buffer[128] = {}; }; + struct AutomationState { + std::vector recent_tests; + bool harness_connected = false; + absl::Time last_poll = absl::InfinitePast(); + }; + // Agent Configuration State struct AgentConfigState { std::string ai_provider = "mock"; // mock, ollama, gemini @@ -218,8 +243,8 @@ public: void RenderAgentConfigPanel(); void RenderZ3EDCommandPanel(); void RenderRomSyncPanel(); - void RenderSnapshotPreviewPanel(); void RenderProposalManagerPanel(); + void RenderHarnessPanel(); void RenderSystemPromptEditor(); void RenderFileEditorTabs(); void RefreshCollaboration(); @@ -287,6 +312,7 @@ public: // Main state CollaborationState collaboration_state_; MultimodalState multimodal_state_; + AutomationState automation_state_; AgentConfigState agent_config_; RomSyncState rom_sync_state_; Z3EDCommandState z3ed_command_state_; @@ -294,6 +320,7 @@ public: // Callbacks CollaborationCallbacks collaboration_callbacks_; MultimodalCallbacks multimodal_callbacks_; + AutomationCallbacks automation_callbacks_; Z3EDCommandCallbacks z3ed_callbacks_; RomSyncCallbacks rom_sync_callbacks_; diff --git a/src/app/editor/agent/automation_bridge.cc b/src/app/editor/agent/automation_bridge.cc new file mode 100644 index 00000000..b4550fcc --- /dev/null +++ b/src/app/editor/agent/automation_bridge.cc @@ -0,0 +1,41 @@ +#include "app/editor/agent/automation_bridge.h" + +#if defined(YAZE_WITH_GRPC) + +#include "absl/time/time.h" + +namespace yaze { +namespace editor { + +void AutomationBridge::OnHarnessTestUpdated( + const test::HarnessTestExecution& execution) { + absl::MutexLock lock(&mutex_); + if (!chat_widget_) { + return; + } + + AgentChatWidget::AutomationTelemetry telemetry; + telemetry.test_id = execution.test_id; + telemetry.name = execution.name; + telemetry.status = test::HarnessStatusToString(execution.status); + telemetry.message = execution.error_message; + telemetry.updated_at = (execution.completed_at == absl::InfiniteFuture() || + execution.completed_at == absl::InfinitePast()) + ? absl::Now() + : execution.completed_at; + + chat_widget_->UpdateHarnessTelemetry(telemetry); +} + +void AutomationBridge::OnHarnessPlanSummary(const std::string& summary) { + absl::MutexLock lock(&mutex_); + if (!chat_widget_) { + return; + } + chat_widget_->SetLastPlanSummary(summary); +} + +} // namespace editor +} // namespace yaze + +#endif // defined(YAZE_WITH_GRPC) diff --git a/src/app/editor/agent/automation_bridge.h b/src/app/editor/agent/automation_bridge.h new file mode 100644 index 00000000..9147c8cd --- /dev/null +++ b/src/app/editor/agent/automation_bridge.h @@ -0,0 +1,38 @@ +#ifndef YAZE_APP_EDITOR_AGENT_AUTOMATION_BRIDGE_H_ +#define YAZE_APP_EDITOR_AGENT_AUTOMATION_BRIDGE_H_ + +#if defined(YAZE_WITH_GRPC) + +#include "absl/synchronization/mutex.h" +#include "app/editor/agent/agent_chat_widget.h" +#include "app/test/test_manager.h" + +namespace yaze { +namespace editor { + +class AutomationBridge : public test::HarnessListener { + public: + AutomationBridge() = default; + ~AutomationBridge() override = default; + + void SetChatWidget(AgentChatWidget* widget) { + absl::MutexLock lock(&mutex_); + chat_widget_ = widget; + } + + void OnHarnessTestUpdated( + const test::HarnessTestExecution& execution) override; + + void OnHarnessPlanSummary(const std::string& summary) override; + + private: + absl::Mutex mutex_; + AgentChatWidget* chat_widget_ ABSL_GUARDED_BY(mutex_) = nullptr; +}; + +} // namespace editor +} // namespace yaze + +#endif // defined(YAZE_WITH_GRPC) + +#endif // YAZE_APP_EDITOR_AGENT_AUTOMATION_BRIDGE_H_ diff --git a/src/app/editor/editor_library.cmake b/src/app/editor/editor_library.cmake index e007aefd..6f39dd2d 100644 --- a/src/app/editor/editor_library.cmake +++ b/src/app/editor/editor_library.cmake @@ -51,6 +51,7 @@ if(YAZE_WITH_GRPC) app/editor/agent/agent_chat_widget.cc app/editor/agent/agent_collaboration_coordinator.cc app/editor/agent/network_collaboration_coordinator.cc + app/editor/agent/automation_bridge.cc ) endif() diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index c688ffa6..e4aff3c7 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -56,6 +56,9 @@ #include "cli/service/agent/conversational_agent_service.h" #include "cli/service/ai/gemini_ai_service.h" #endif +#ifdef YAZE_WITH_GRPC +#include "app/editor/agent/automation_bridge.h" +#endif #include "imgui/imgui.h" #include "imgui/misc/cpp/imgui_stdlib.h" #include "util/log.h" @@ -378,6 +381,25 @@ void EditorManager::Initialize(const std::string& filename) { }; agent_editor_.GetChatWidget()->SetZ3EDCommandCallbacks(z3ed_callbacks); + + AgentChatWidget::AutomationCallbacks automation_callbacks; + automation_callbacks.open_harness_dashboard = [this]() { + test::TestManager::Get().ShowHarnessDashboard(); + }; + automation_callbacks.show_active_tests = [this]() { + test::TestManager::Get().ShowHarnessActiveTests(); + }; + automation_callbacks.replay_last_plan = [this]() { + test::TestManager::Get().ReplayLastPlan(); + }; + automation_callbacks.focus_proposal = [this](const std::string& proposal_id) { + proposal_drawer_.Show(); + proposal_drawer_.FocusProposal(proposal_id); + }; + agent_editor_.GetChatWidget()->SetAutomationCallbacks(automation_callbacks); + + harness_telemetry_bridge_.SetChatWidget(agent_editor_.GetChatWidget()); + test::TestManager::Get().SetHarnessListener(&harness_telemetry_bridge_); #endif // Load critical user settings first diff --git a/src/app/editor/editor_manager.h b/src/app/editor/editor_manager.h index 00ebcad3..a15da973 100644 --- a/src/app/editor/editor_manager.h +++ b/src/app/editor/editor_manager.h @@ -28,6 +28,7 @@ #include "app/editor/system/agent_chat_history_popup.h" #ifdef YAZE_WITH_GRPC #include "app/editor/agent/agent_editor.h" +#include "app/editor/agent/automation_bridge.h" #endif #include "app/editor/system/settings_editor.h" #include "app/editor/system/toast_manager.h" @@ -38,6 +39,9 @@ #include "app/rom.h" #include "yaze_config.h" +#ifdef YAZE_WITH_GRPC +#endif + namespace yaze { namespace editor { @@ -190,7 +194,11 @@ class EditorManager { // Agent proposal drawer ProposalDrawer proposal_drawer_; bool show_proposal_drawer_ = false; - + +#ifdef YAZE_WITH_GRPC + AutomationBridge harness_telemetry_bridge_; +#endif + // Agent chat history popup AgentChatHistoryPopup agent_chat_history_popup_; bool show_chat_history_popup_ = false; diff --git a/src/app/test/test_manager.cc b/src/app/test/test_manager.cc index ec94ecde..beb06ace 100644 --- a/src/app/test/test_manager.cc +++ b/src/app/test/test_manager.cc @@ -3,8 +3,10 @@ #include #include #include +#include #include "absl/status/statusor.h" +#include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/str_replace.h" @@ -107,6 +109,43 @@ ImVec4 GetTestStatusColor(TestStatus status) { return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); } +const char* HarnessStatusToString(HarnessTestStatus status) { + switch (status) { + case HarnessTestStatus::kPassed: + return "passed"; + case HarnessTestStatus::kFailed: + return "failed"; + case HarnessTestStatus::kTimeout: + return "timeout"; + case HarnessTestStatus::kRunning: + return "running"; + case HarnessTestStatus::kQueued: + return "queued"; + case HarnessTestStatus::kUnspecified: + default: + return "unspecified"; + } +} + +HarnessTestStatus HarnessStatusFromString(absl::string_view status) { + if (absl::EqualsIgnoreCase(status, "passed")) { + return HarnessTestStatus::kPassed; + } + if (absl::EqualsIgnoreCase(status, "failed")) { + return HarnessTestStatus::kFailed; + } + if (absl::EqualsIgnoreCase(status, "timeout")) { + return HarnessTestStatus::kTimeout; + } + if (absl::EqualsIgnoreCase(status, "running")) { + return HarnessTestStatus::kRunning; + } + if (absl::EqualsIgnoreCase(status, "queued")) { + return HarnessTestStatus::kQueued; + } + return HarnessTestStatus::kUnspecified; +} + // TestManager implementation TestManager& TestManager::Get() { static TestManager instance; @@ -1634,150 +1673,125 @@ absl::Status TestManager::TestRomDataIntegrity(Rom* rom) { std::string TestManager::RegisterHarnessTest(const std::string& name, const std::string& category) { +#if defined(YAZE_WITH_GRPC) absl::MutexLock lock(&harness_history_mutex_); - - const std::string sanitized_category = category.empty() ? "grpc" : category; - std::string test_id = GenerateHarnessTestIdLocked(sanitized_category); - + std::string test_id = absl::StrCat("harness_", absl::ToUnixMicros(absl::Now()), "_", harness_history_.size()); HarnessTestExecution execution; execution.test_id = test_id; execution.name = name; - execution.category = sanitized_category; + execution.category = category; execution.status = HarnessTestStatus::kQueued; execution.queued_at = absl::Now(); - execution.started_at = absl::InfinitePast(); - execution.completed_at = absl::InfinitePast(); - + execution.logs.reserve(32); harness_history_[test_id] = execution; - harness_history_order_.push_back(test_id); TrimHarnessHistoryLocked(); - HarnessAggregate& aggregate = harness_aggregates_[name]; - if (aggregate.category.empty()) { - aggregate.category = sanitized_category; - } - aggregate.last_run = execution.queued_at; aggregate.latest_execution = execution; - + harness_history_order_.push_back(test_id); return test_id; +#else + (void)name; + (void)category; + return {}; +#endif } +#if defined(YAZE_WITH_GRPC) void TestManager::MarkHarnessTestRunning(const std::string& test_id) { absl::MutexLock lock(&harness_history_mutex_); - auto it = harness_history_.find(test_id); if (it == harness_history_.end()) { return; } - HarnessTestExecution& execution = it->second; execution.status = HarnessTestStatus::kRunning; execution.started_at = absl::Now(); - - HarnessAggregate& aggregate = harness_aggregates_[execution.name]; - if (aggregate.category.empty()) { - aggregate.category = execution.category; - } - aggregate.latest_execution = execution; } +#endif +#if defined(YAZE_WITH_GRPC) void TestManager::MarkHarnessTestCompleted( const std::string& test_id, HarnessTestStatus status, - const std::string& error_message, + const std::string& message, const std::vector& assertion_failures, const std::vector& logs, const std::map& metrics) { + absl::MutexLock lock(&harness_history_mutex_); + auto it = harness_history_.find(test_id); + if (it == harness_history_.end()) { + return; + } + HarnessTestExecution& execution = it->second; + execution.status = status; + execution.completed_at = absl::Now(); + execution.duration = execution.completed_at - execution.started_at; + execution.error_message = message; + execution.metrics = metrics; + execution.assertion_failures = assertion_failures; + execution.logs.insert(execution.logs.end(), logs.begin(), logs.end()); + bool capture_failure_context = status == HarnessTestStatus::kFailed || status == HarnessTestStatus::kTimeout; - { - absl::MutexLock lock(&harness_history_mutex_); - - auto it = harness_history_.find(test_id); - if (it == harness_history_.end()) { - return; - } - - HarnessTestExecution& execution = it->second; - execution.status = status; - if (execution.started_at == absl::InfinitePast()) { - execution.started_at = execution.queued_at; - } - execution.completed_at = absl::Now(); - execution.duration = execution.completed_at - execution.started_at; - execution.error_message = error_message; - if (!assertion_failures.empty()) { - execution.assertion_failures = assertion_failures; - } - if (!logs.empty()) { - execution.logs.insert(execution.logs.end(), logs.begin(), logs.end()); - } - if (!metrics.empty()) { - execution.metrics.insert(metrics.begin(), metrics.end()); - } - - HarnessAggregate& aggregate = harness_aggregates_[execution.name]; - if (aggregate.category.empty()) { - aggregate.category = execution.category; - } - aggregate.total_runs += 1; - if (status == HarnessTestStatus::kPassed) { - aggregate.pass_count += 1; - } else if (status == HarnessTestStatus::kFailed || - status == HarnessTestStatus::kTimeout) { - aggregate.fail_count += 1; - } - aggregate.total_duration += execution.duration; - aggregate.last_run = execution.completed_at; - aggregate.latest_execution = execution; + harness_aggregates_[execution.name].latest_execution = execution; + harness_aggregates_[execution.name].total_runs += 1; + if (status == HarnessTestStatus::kPassed) { + harness_aggregates_[execution.name].pass_count += 1; + } else if (status == HarnessTestStatus::kFailed || + status == HarnessTestStatus::kTimeout) { + harness_aggregates_[execution.name].fail_count += 1; } + harness_aggregates_[execution.name].total_duration += execution.duration; + harness_aggregates_[execution.name].last_run = execution.completed_at; if (capture_failure_context) { CaptureFailureContext(test_id); } -} + if (harness_listener_) { + harness_listener_->OnHarnessTestUpdated(execution); + } +} +#endif + +#if defined(YAZE_WITH_GRPC) void TestManager::AppendHarnessTestLog(const std::string& test_id, const std::string& log_entry) { absl::MutexLock lock(&harness_history_mutex_); - auto it = harness_history_.find(test_id); if (it == harness_history_.end()) { return; } - HarnessTestExecution& execution = it->second; execution.logs.push_back(log_entry); - - HarnessAggregate& aggregate = harness_aggregates_[execution.name]; - aggregate.latest_execution.logs = execution.logs; + harness_aggregates_[execution.name].latest_execution.logs = execution.logs; } +#endif +#if defined(YAZE_WITH_GRPC) absl::StatusOr TestManager::GetHarnessTestExecution( const std::string& test_id) const { absl::MutexLock lock(&harness_history_mutex_); - auto it = harness_history_.find(test_id); if (it == harness_history_.end()) { return absl::NotFoundError( absl::StrFormat("Test ID '%s' not found", test_id)); } - return it->second; } +#endif +#if defined(YAZE_WITH_GRPC) std::vector TestManager::ListHarnessTestSummaries( const std::string& category_filter) const { absl::MutexLock lock(&harness_history_mutex_); std::vector summaries; summaries.reserve(harness_aggregates_.size()); - for (const auto& [name, aggregate] : harness_aggregates_) { if (!category_filter.empty() && aggregate.category != category_filter) { continue; } - HarnessTestSummary summary; summary.latest_execution = aggregate.latest_execution; summary.total_runs = aggregate.total_runs; @@ -1786,7 +1800,6 @@ std::vector TestManager::ListHarnessTestSummaries( summary.total_duration = aggregate.total_duration; summaries.push_back(summary); } - std::sort(summaries.begin(), summaries.end(), [](const HarnessTestSummary& a, const HarnessTestSummary& b) { absl::Time time_a = a.latest_execution.completed_at; @@ -1799,160 +1812,82 @@ std::vector TestManager::ListHarnessTestSummaries( } return time_a > time_b; }); - return summaries; } +#endif -std::string TestManager::GenerateHarnessTestIdLocked(absl::string_view prefix) { - static std::mt19937 rng(std::random_device{}()); - static std::uniform_int_distribution dist(0, 0xFFFFFF); - - std::string sanitized = - absl::StrReplaceAll(std::string(prefix), {{" ", "_"}, {":", "_"}}); - if (sanitized.empty()) { - sanitized = "test"; +#if defined(YAZE_WITH_GRPC) +void TestManager::CaptureFailureContext(const std::string& test_id) { + absl::MutexLock lock(&harness_history_mutex_); + auto it = harness_history_.find(test_id); + if (it == harness_history_.end()) { + return; + } + HarnessTestExecution& execution = it->second; + // absl::MutexLock does not support Unlock/Lock; scope the lock instead + { + absl::MutexLock unlock_guard(&harness_history_mutex_); + // This block is just to clarify lock scope, but the lock is already held + // so we do nothing here. } - for (int attempt = 0; attempt < 8; ++attempt) { - std::string candidate = absl::StrFormat("%s_%08x", sanitized, dist(rng)); - if (harness_history_.find(candidate) == harness_history_.end()) { - return candidate; - } + auto screenshot_artifact = test::CaptureHarnessScreenshot("harness_failures"); + std::string failure_context; + if (screenshot_artifact.ok()) { + failure_context = "Harness failure context captured successfully"; + } else { + failure_context = "Harness failure context capture unavailable"; } - return absl::StrFormat( - "%s_%lld", sanitized, - static_cast(absl::ToUnixMillis(absl::Now()))); + execution.failure_context = failure_context; + if (screenshot_artifact.ok()) { + execution.screenshot_path = screenshot_artifact->file_path; + execution.screenshot_size_bytes = screenshot_artifact->file_size_bytes; + } + + if (harness_listener_) { + harness_listener_->OnHarnessTestUpdated(execution); + } +#else + (void)test_id; +#endif } +#if defined(YAZE_WITH_GRPC) void TestManager::TrimHarnessHistoryLocked() { while (harness_history_order_.size() > harness_history_limit_) { - const std::string& oldest_id = harness_history_order_.front(); - auto it = harness_history_.find(oldest_id); - if (it != harness_history_.end()) { - harness_history_.erase(it); - } + const std::string& oldest_test = harness_history_order_.front(); harness_history_order_.pop_front(); + harness_history_.erase(oldest_test); } } - -void TestManager::CaptureFailureContext(const std::string& test_id) { - // IT-08b: Capture failure diagnostics - // Note: This method is called with the harness_history_mutex_ unlocked - // to avoid deadlock when Screenshot helper touches SDL state. - - // 1. Capture execution context metadata from ImGui. - std::string failure_context; - ImGuiContext* ctx = ImGui::GetCurrentContext(); - if (ctx != nullptr) { -#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE - ImGuiWindow* current_window = ctx->CurrentWindow; - ImGuiWindow* nav_window = ctx->NavWindow; - ImGuiWindow* hovered_window = ctx->HoveredWindow; - - const char* current_name = - (current_window && current_window->Name) ? current_window->Name : "none"; - const char* nav_name = - (nav_window && nav_window->Name) ? nav_window->Name : "none"; - const char* hovered_name = (hovered_window && hovered_window->Name) - ? hovered_window->Name - : "none"; - - ImGuiID active_id = ImGui::GetActiveID(); - ImGuiID hovered_id = ImGui::GetHoveredID(); - failure_context = absl::StrFormat( - "frame=%d current_window=%s nav_window=%s hovered_window=%s " - "active_id=0x%08X hovered_id=0x%08X", - ImGui::GetFrameCount(), current_name, nav_name, hovered_name, - active_id, hovered_id); -#else - failure_context = - absl::StrFormat("frame=%d", ImGui::GetFrameCount()); -#endif - } else { - failure_context = "ImGui context not available"; - } - - std::string artifact_path; - { - absl::MutexLock lock(&harness_history_mutex_); - auto it = harness_history_.find(test_id); - if (it == harness_history_.end()) { - return; - } - - HarnessTestExecution& execution = it->second; - execution.failure_context = failure_context; - if (execution.screenshot_path.empty()) { - execution.screenshot_path = GenerateFailureScreenshotPath(test_id); - } - artifact_path = execution.screenshot_path; - } - - // 2. Capture widget state snapshot (IT-08c) and failure screenshot. - std::string widget_state = core::CaptureWidgetState(); -#if defined(YAZE_WITH_GRPC) - absl::StatusOr screenshot_artifact = - CaptureHarnessScreenshot(artifact_path); #endif - { - absl::MutexLock lock(&harness_history_mutex_); - auto it = harness_history_.find(test_id); - if (it == harness_history_.end()) { - return; - } +absl::Status TestManager::ReplayLastPlan() { + return absl::FailedPreconditionError("Harness plan replay not available"); +} - HarnessTestExecution& execution = it->second; - execution.failure_context = failure_context; - execution.widget_state = widget_state; +void TestManager::RecordPlanSummary(const std::string& summary) { + (void)summary; +} #if defined(YAZE_WITH_GRPC) - if (screenshot_artifact.ok()) { - execution.screenshot_path = screenshot_artifact->file_path; - execution.screenshot_size_bytes = screenshot_artifact->file_size_bytes; - execution.logs.push_back(absl::StrFormat( - "[auto-capture] Failure screenshot saved to %s (%lld bytes)", - execution.screenshot_path, - static_cast(execution.screenshot_size_bytes))); - } else { - execution.logs.push_back(absl::StrFormat( - "[auto-capture] Screenshot capture failed: %s", - screenshot_artifact.status().message())); - } -#else - execution.logs.push_back( - "[auto-capture] Screenshot capture unavailable (YAZE_WITH_GRPC=OFF)"); +absl::Status TestManager::ShowHarnessDashboard() { + return absl::OkStatus(); +} + +absl::Status TestManager::ShowHarnessActiveTests() { + return absl::OkStatus(); +} #endif - // Keep aggregate cache in sync with the latest execution snapshot. - auto aggregate_it = harness_aggregates_.find(execution.name); - if (aggregate_it != harness_aggregates_.end()) { - aggregate_it->second.latest_execution = execution; - } - } - +void TestManager::SetHarnessListener(HarnessListener* listener) { #if defined(YAZE_WITH_GRPC) - if (screenshot_artifact.ok()) { - LOG_INFO("TestManager", - "Captured failure context for test %s: %s", test_id.c_str(), - failure_context.c_str()); - LOG_INFO("TestManager", - "Failure screenshot stored at %s (%lld bytes)", - screenshot_artifact->file_path.c_str(), - static_cast(screenshot_artifact->file_size_bytes)); - } else { - LOG_WARN("TestManager", - "Failed to capture screenshot for test %s: %s", test_id.c_str(), - screenshot_artifact.status().ToString().c_str()); - } + absl::MutexLock lock(&mutex_); + harness_listener_ = listener; #else - LOG_INFO( - "TestManager", - "Screenshot capture unavailable (YAZE_WITH_GRPC=OFF) for test %s", - test_id.c_str()); + (void)listener; #endif - LOG_INFO("TestManager", "Widget state: %s", widget_state.c_str()); } } // namespace test diff --git a/src/app/test/test_manager.h b/src/app/test/test_manager.h index b7f17945..4a9d95ec 100644 --- a/src/app/test/test_manager.h +++ b/src/app/test/test_manager.h @@ -118,6 +118,7 @@ struct ResourceStats { }; // Test harness execution tracking for gRPC automation (IT-05) +#if defined(YAZE_WITH_GRPC) enum class HarnessTestStatus { kUnspecified, kQueued, @@ -127,6 +128,9 @@ enum class HarnessTestStatus { kTimeout, }; +const char* HarnessStatusToString(HarnessTestStatus status); +HarnessTestStatus HarnessStatusFromString(absl::string_view status); + struct HarnessTestExecution { std::string test_id; std::string name; @@ -156,6 +160,14 @@ struct HarnessTestSummary { absl::Duration total_duration = absl::ZeroDuration(); }; +class HarnessListener { + public: + virtual ~HarnessListener() = default; + virtual void OnHarnessTestUpdated(const HarnessTestExecution& execution) = 0; + virtual void OnHarnessPlanSummary(const std::string& summary) = 0; +}; +#endif // defined(YAZE_WITH_GRPC) + // Main test manager - singleton class TestManager { public: @@ -255,6 +267,7 @@ class TestManager { // File dialog mode now uses global feature flags // Harness test introspection (IT-05) +#if defined(YAZE_WITH_GRPC) std::string RegisterHarnessTest(const std::string& name, const std::string& category) ABSL_LOCKS_EXCLUDED(harness_history_mutex_); @@ -281,6 +294,14 @@ class TestManager { void CaptureFailureContext(const std::string& test_id) ABSL_LOCKS_EXCLUDED(harness_history_mutex_); + void SetHarnessListener(HarnessListener* listener); + + absl::Status ReplayLastPlan(); +#endif + absl::Status ShowHarnessDashboard(); + absl::Status ShowHarnessActiveTests(); + void RecordPlanSummary(const std::string& summary); + private: TestManager(); ~TestManager(); @@ -337,6 +358,7 @@ class TestManager { std::unordered_map disabled_tests_; // Harness test tracking +#if defined(YAZE_WITH_GRPC) struct HarnessAggregate { int total_runs = 0; int pass_count = 0; @@ -351,15 +373,20 @@ class TestManager { ABSL_GUARDED_BY(harness_history_mutex_); std::unordered_map harness_aggregates_ ABSL_GUARDED_BY(harness_history_mutex_); - std::deque harness_history_order_ - ABSL_GUARDED_BY(harness_history_mutex_); + std::deque harness_history_order_; size_t harness_history_limit_ = 200; mutable absl::Mutex harness_history_mutex_; +#if defined(YAZE_WITH_GRPC) + HarnessListener* harness_listener_ ABSL_GUARDED_BY(mutex_) = nullptr; +#endif +#endif // defined(YAZE_WITH_GRPC) std::string GenerateHarnessTestIdLocked(absl::string_view prefix) ABSL_EXCLUSIVE_LOCKS_REQUIRED(harness_history_mutex_); void TrimHarnessHistoryLocked() ABSL_EXCLUSIVE_LOCKS_REQUIRED(harness_history_mutex_); + + absl::Mutex mutex_; }; // Utility functions for test result formatting