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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# z3ed Networking, Collaboration, and Remote Access
|
||||
# Networking and Collaboration
|
||||
|
||||
**Version**: 0.2.0-alpha
|
||||
**Last Updated**: October 5, 2025
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ absl::StatusOr<TestRecorder::StopRecordingSummary> 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 {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <vector>
|
||||
|
||||
#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<int>(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<std::string> AgentChatWidget::GetSavedSessions() {
|
||||
std::vector<std::string> 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
|
||||
|
||||
@@ -67,6 +67,21 @@ class AgentChatWidget {
|
||||
std::function<absl::Status(const std::filesystem::path&, const std::string&)> send_to_gemini;
|
||||
};
|
||||
|
||||
struct AutomationCallbacks {
|
||||
std::function<void()> open_harness_dashboard;
|
||||
std::function<void()> replay_last_plan;
|
||||
std::function<void(const std::string&)> focus_proposal;
|
||||
std::function<void()> 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<absl::Status(const std::string&)> run_agent_task;
|
||||
@@ -84,6 +99,8 @@ class AgentChatWidget {
|
||||
std::function<std::string()> 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<AutomationTelemetry> 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_;
|
||||
|
||||
|
||||
41
src/app/editor/agent/automation_bridge.cc
Normal file
41
src/app/editor/agent/automation_bridge.cc
Normal file
@@ -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)
|
||||
38
src/app/editor/agent/automation_bridge.h
Normal file
38
src/app/editor/agent/automation_bridge.h
Normal file
@@ -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_
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <random>
|
||||
#include <optional>
|
||||
|
||||
#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<std::string>& assertion_failures,
|
||||
const std::vector<std::string>& logs,
|
||||
const std::map<std::string, int32_t>& 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<HarnessTestExecution> 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<HarnessTestSummary> TestManager::ListHarnessTestSummaries(
|
||||
const std::string& category_filter) const {
|
||||
absl::MutexLock lock(&harness_history_mutex_);
|
||||
std::vector<HarnessTestSummary> 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<HarnessTestSummary> 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<HarnessTestSummary> 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<uint32_t> 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<long long>(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<ScreenshotArtifact> 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<long long>(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<long long>(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
|
||||
|
||||
@@ -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<std::string, bool> 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<std::string, HarnessAggregate> harness_aggregates_
|
||||
ABSL_GUARDED_BY(harness_history_mutex_);
|
||||
std::deque<std::string> harness_history_order_
|
||||
ABSL_GUARDED_BY(harness_history_mutex_);
|
||||
std::deque<std::string> 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
|
||||
|
||||
Reference in New Issue
Block a user