diff --git a/docs/03-asar-integration.md b/docs/03-asar-integration.md index 34c104ce..a021ade5 100644 --- a/docs/03-asar-integration.md +++ b/docs/03-asar-integration.md @@ -20,7 +20,7 @@ z3ed validate my_patch.asm ```cpp #include "app/core/asar_wrapper.h" -yaze::app::core::AsarWrapper wrapper; +yaze::core::AsarWrapper wrapper; wrapper.Initialize(); // Apply patch to ROM diff --git a/docs/04-api-reference.md b/docs/04-api-reference.md index 97f17da6..9b26b1bf 100644 --- a/docs/04-api-reference.md +++ b/docs/04-api-reference.md @@ -59,7 +59,7 @@ yaze_status yaze_save_message(zelda3_rom* rom, const zelda3_message* message); ### AsarWrapper (`src/app/core/asar_wrapper.h`) ```cpp -namespace yaze::app::core { +namespace yaze::core { class AsarWrapper { public: diff --git a/docs/B1-contributing.md b/docs/B1-contributing.md index 9e06efd3..51af7466 100644 --- a/docs/B1-contributing.md +++ b/docs/B1-contributing.md @@ -41,7 +41,7 @@ ctest --preset stable #include "app/core/asar_wrapper.h" // Namespace usage -namespace yaze::app::editor { +namespace yaze::editor { class ExampleClass { public: diff --git a/docs/z3ed/E6-z3ed-cli-design.md b/docs/z3ed/E6-z3ed-cli-design.md index 56af8fbc..e5d2b13e 100644 --- a/docs/z3ed/E6-z3ed-cli-design.md +++ b/docs/z3ed/E6-z3ed-cli-design.md @@ -27,8 +27,8 @@ This document is the **source of truth** for the z3ed CLI architecture and desig - **Test Harness Enhancements (IT-05 to IT-09)**: Expanding from basic automation to comprehensive testing platform - Test introspection APIs for status/results polling - Widget discovery for AI-driven interactions - - Test recording/replay for regression testing - - Enhanced error reporting with screenshots + - **βœ… Test recording/replay for regression testing** + - Enhanced error reporting with screenshots and application-wide diagnostics - CI/CD integration with standardized test formats **πŸ“‹ Planned Next**: @@ -317,64 +317,23 @@ available_actions = [w.suggested_action for w in widgets.buttons if w.is_enabled z3ed_client.Click(target="button:Save Changes") ``` -#### IT-07: Test Recording & Replay (8-10 hours) -**Problem**: No way to capture manual workflows for regression. Testers repeat same actions every release. +#### IT-07: Test Recording & Replay βœ… COMPLETE +**Outcome**: Recording workflow, replay runner, and JSON script format shipped alongside CLI commands (`z3ed test record start|stop`, `z3ed test replay`). Regression coverage captured in `scripts/test_record_replay_e2e.sh`; documentation updated with quick-start examples. Focus now shifts to error diagnostics and artifact surfacing (IT-08). -**Solution**: Add recording workflow: -- `StartRecording(output_file)` β†’ Begins capturing all RPC calls -- `StopRecording()` β†’ Saves to JSON test script -- `ReplayTest(test_script)` β†’ Executes recorded actions with validation +#### IT-08: Holistic Error Reporting (5-7 hours) +**Problem**: Errors surface differently across the CLI, ImGuiTestHarness, and EditorManager. Failures lack actionable context, slowing down triage and AI agent autonomy. -**Test Script Format** (JSON): -```json -{ - "name": "Overworld Tile Edit Test", - "steps": [ - { "action": "Click", "target": "menuitem: Overworld Editor" }, - { "action": "Wait", "condition": "window_visible:Overworld", "timeout_ms": 5000 }, - { "action": "Click", "target": "button:Select Tile" }, - { "action": "Assert", "condition": "enabled:button:Apply" } - ] -} -``` +**Solution Themes**: +- **Harness Diagnostics**: Implement the Screenshot RPC, capture widget tree/state, and bundle execution context for every failed run. +- **Structured Error Envelope**: Introduce a shared `ErrorAnnotatedResult` format (status + metadata + hints) adopted by z3ed, harness services, and EditorManager subsystems. +- **Artifact Surfacing**: Persist artifacts under `test-results//`; expose paths in CLI output and in-app overlays. +- **Developer Experience**: Provide HTML + JSON result formats, actionable hints (β€œRe-run with --follow”, β€œOpen screenshot: …”), and cross-links to recorded sessions for replay. **Benefits**: -- QA engineers record test scenarios once, replay forever -- Test scripts version controlled alongside code -- Parameterized tests (e.g., test with different ROMs) -- Foundation for test suite management (smoke, regression, nightly) - -#### IT-08: Enhanced Error Reporting (3-4 hours) -**Problem**: Test failures lack context. Developer sees "Window not visible" but doesn't know why. - -**Solution**: Capture rich context on failure: -- Screenshot (implement stub RPC) -- Widget state dump (full hierarchy with properties) -- Execution context (active window, recent events, resource stats) -- HTML report generation with annotated screenshots - -**Example Error Report**: -```json -{ - "test_id": "grpc_wait_12345678", - "failure_reason": "Timeout waiting for window_visible:Overworld", - "screenshot": "test-results/failure_12345678.png", - "widget_state": { - "visible_windows": ["Main Window", "Debug"], - "overworld_window": { "exists": true, "visible": false, "reason": "not_initialized" } - }, - "execution_context": { - "last_click": "menuitem: Overworld Editor", - "frames_since_click": 150, - "resource_stats": { "memory_mb": 245, "framerate": 58.3 } - } -} -``` - -**Benefits**: -- Developers fix failing tests faster (visual + state context) -- Flaky test debugging (see exact UI state at failure) -- Test reports shareable with QA/PM (HTML with screenshots) +- Faster debugging with consistent, high-signal failure context +- AI agents can reason about structured errors and attempt self-healing +- EditorManager gains on-screen diagnostics tied to harness artifacts +- Lays groundwork for future telemetry and CI reporting #### IT-09: CI/CD Integration (2-3 hours) **Problem**: Tests run manually. No automated regression on PR/merge. diff --git a/docs/z3ed/E6-z3ed-implementation-plan.md b/docs/z3ed/E6-z3ed-implementation-plan.md index a62dc10f..1df61230 100644 --- a/docs/z3ed/E6-z3ed-implementation-plan.md +++ b/docs/z3ed/E6-z3ed-implementation-plan.md @@ -17,14 +17,14 @@ The z3ed CLI and AI agent workflow system has completed major infrastructure mil - **IT-02**: CLI Agent Test - Natural language β†’ automated GUI testing (implementation complete) **πŸ”„ Active Phase**: -- **Test Harness Enhancements (IT-05 to IT-09)**: Expanding from basic automation to comprehensive testing platform +- **Test Harness Enhancements (IT-05 to IT-09)**: Expanding from basic automation to comprehensive testing platform with a renewed emphasis on system-wide error reporting **πŸ“‹ Next Phases**: - **Priority 1**: Test Introspection API (IT-05) - Enable test status querying and result polling - **Priority 2**: Widget Discovery API (IT-06) - AI agents enumerate available GUI interactions -- **Priority 3**: Test Recording & Replay (IT-07) - Capture workflows for regression testing +- **Priority 3**: Enhanced Error Reporting (IT-08+) - Holistic improvements spanning z3ed, ImGuiTestHarness, EditorManager, and core application services -**Recent Accomplishments** (Updated: January 2025): +**Recent Accomplishments** (Updated: October 2025): - **βœ… Policy Framework Complete**: PolicyEvaluator service fully integrated with ProposalDrawer GUI - 4 policy types implemented: test_requirement, change_constraint, forbidden_range, review_requirement - 3 severity levels: Info (informational), Warning (overridable), Critical (blocks acceptance) @@ -36,6 +36,7 @@ The z3ed CLI and AI agent workflow system has completed major infrastructure mil - Thread safety issues **resolved** with shared_ptr state management - Test harness validated on macOS ARM64 with real YAZE GUI interactions - **gRPC Test Harness (IT-01 & IT-02)**: Full implementation complete with natural language β†’ GUI testing +- **βœ… Test Recording & Replay (IT-07)**: JSON recorder/replayer implemented, CLI and harness wired, end-to-end regression workflow captured in `scripts/test_record_replay_e2e.sh` - **Build System**: Hardened CMake configuration with reliable gRPC integration - **Proposal Workflow**: Agentic proposal system fully operational (create, list, diff, review in GUI) @@ -84,16 +85,16 @@ The z3ed CLI and AI agent workflow system has completed major infrastructure mil **Status**: Core Infrastructure Complete βœ… | Test Harness Enhancement Phase πŸ”§ ### Priority 1: Test Harness Enhancements (IT-05 to IT-09) πŸ”§ ACTIVE -**Goal**: Transform test harness from basic automation to comprehensive testing platform -**Time Estimate**: 20-25 hours total +**Goal**: Transform test harness from basic automation to comprehensive testing platform **and deliver holistic error reporting across YAZE** +**Time Estimate**: 20-25 hours total (7.5h completed in IT-07) **Blocking Dependency**: IT-01 Complete βœ… -**Motivation**: Current test harness supports basic GUI automation but lacks features for: -- **AI Agent Development**: No widget discovery API for LLMs to learn available interactions -- **Regression Testing**: No recording/replay mechanism for test suite management -- **CI/CD Integration**: No standardized test format for automated pipelines -- **Debugging**: Limited error context when tests fail (no screenshots, state dumps) -- **Test Management**: Can't query test status, results, or execution queue +**Motivation**: The harness now supports AI workflows, regression capture, and automationβ€”but error surfaces remain shallow: +- **AI Agent Development**: Still needs widget discovery for adaptive planning +- **Regression Testing**: Recording/replay finished; reporting pipeline must surface actionable failures +- **CI/CD Integration**: Requires reliable artifacts (logs, screenshots, structured context) +- **Debugging**: Failures lack screenshots, widget hierarchies, and EditorManager state snapshots +- **Application Consistency**: z3ed, EditorManager, and core services emit heterogeneous error formats #### IT-05: Test Introspection API (6-8 hours) **Status (Oct 2, 2025)**: 🟑 *Server-side RPCs implemented; CLI + E2E pending* @@ -224,84 +225,42 @@ message WidgetInfo { - Agents can adapt to UI changes without hardcoded widget names - Natural language descriptions enable better prompt engineering -#### IT-07: Test Recording & Replay (8-10 hours) -**Implementation Tasks**: -1. **Add StartRecording/StopRecording RPCs**: - - Capture all RPC calls during a session - - Record timing, parameters, and results - - Save to JSON test script format - -2. **Add ReplayTest RPC**: - - Load JSON test script - - Execute recorded actions sequentially - - Validate expected results match actual results - - Support parameterization (e.g., replace ROM filename) - -3. **Test Script Format**: - - Human-readable JSON with comments - - Support assertions and conditionals - - Enable test suite composition (call other scripts) +#### IT-07: Test Recording & Replay βœ… COMPLETE (Oct 2, 2025) +**Highlights**: +- Implemented `StartRecording`, `StopRecording`, and `ReplayTest` RPCs with persistent JSON scripts +- Added CLI commands: `z3ed test record start|stop`, `z3ed test replay` +- Scripts stored in `tests/gui/` with metadata (name, tags, assertions, timing hints) +- Added regression coverage via `scripts/test_record_replay_e2e.sh` +- Documentation updates in `E6-z3ed-reference.md` and new quick-start snippets in README +- Confirmed compatibility with natural language prompts generated by the agent workflow -**Example Workflow**: -```bash -# Start recording -z3ed test record start --output overworld_test.json +**Outcome**: Recording/replay is production-ready; focus shifts to surfacing rich failure diagnostics (IT-08). -# Perform actions (manually or via agent) -z3ed agent test --prompt "Open Overworld editor" -z3ed agent test --prompt "Click tile at 10,20" +#### IT-08: Enhanced Error Reporting (5-7 hours) +**Objective**: Deliver a unified, high-signal error reporting pipeline spanning ImGuiTestHarness, z3ed CLI, EditorManager, and core application services. -# Stop recording -z3ed test record stop +**Implementation Tracks**: +1. **Harness-Level Diagnostics** + - Implement Screenshot RPC (convert stub into working SDL capture pipeline) + - Auto-capture screenshots, widget tree dumps, and recent ImGui events on failure + - Serialize results to both structured JSON (for automation) and human-friendly HTML bundles + - Persist artifacts under `test-results//` with timestamped directories -# Replay test -z3ed test replay overworld_test.json +2. **CLI Experience Improvements** + - Standardize error envelopes in z3ed (`absl::Status` + structured payload) + - Surface artifact paths, summarized failure reason, and next-step hints in CLI output + - Add `--format html` / `--format json` flags to `z3ed agent test results` to emit richer context + - Integrate with recording workflow: replay failures using captured state for fast reproduction -# Run in CI -z3ed test replay tests/*.json --ci-mode -``` +3. **EditorManager & Application Integration** + - Introduce shared `ErrorAnnotatedResult` utility exposing `status`, `context`, `actionable_hint` + - Adapt EditorManager subsystems (ProposalDrawer, OverworldEditor, DungeonEditor) to adopt the shared structure + - Add in-app failure overlay (ImGui modal) that references harness artifacts when available + - Hook proposal acceptance/replay flows to display enriched diagnostics when sandbox merges fail -**JSON Test Script Example**: -```json -{ - "name": "Overworld Editor Load Test", - "description": "Verify Overworld editor opens and tile selection works", - "steps": [ - { - "action": "Click", - "target": "menuitem: Overworld Editor", - "expected_result": { "success": true } - }, - { - "action": "Wait", - "condition": "window_visible:Overworld", - "timeout_ms": 5000 - }, - { - "action": "Assert", - "condition": "visible:Overworld", - "expected": { "success": true, "actual_value": "visible" } - } - ] -} -``` - -#### IT-08: Enhanced Error Reporting (3-4 hours) -**Implementation Tasks**: -1. **Screenshot on Failure**: - - Implement Screenshot RPC (complete stub) - - Automatically capture screenshot when test fails - - Save to proposal directory or test results folder - -2. **Widget State Dumps**: - - Capture full widget tree on assertion failure - - Include widget properties (enabled, visible, position, text) - - Generate HTML report with annotated screenshots - -3. **Execution Context**: - - Log ImGui state: active window, focused widget, frame count - - Capture recent ImGui events (clicks, key presses, hovers) - - Include resource stats: memory, textures, framerate +4. **Telemetry & Storage Hooks** (Stretch) + - Optionally emit error metadata to a ring buffer for future analytics/telemetry workstreams + - Provide CLI flag `--error-artifact-dir` to customize storage (supports CI separation) **Error Report Example**: ```json @@ -321,7 +280,12 @@ z3ed test replay tests/*.json --ci-mode "execution_context": { "frame_count": 1234, "recent_events": ["Click: menuitem: Overworld Editor", "Wait: window_visible:Overworld"], - "resource_stats": { "memory_mb": 245, "textures": 12, "framerate": 60.0 } + "resource_stats": { "memory_mb": 245, "textures": 12, "framerate": 60.0 }, + "editor_manager_snapshot": { + "active_module": "OverworldEditor", + "dirty_buffers": ["overworld_layer_1"], + "last_error": null + } } } ``` @@ -463,8 +427,10 @@ jobs: | IT-04 | Complete E2E validation with real YAZE widgets | ImGuiTest Bridge | Test | βœ… Done | IT-02 - All 5 functional tests passing, window detection fixed with yield buffer | | IT-05 | Add test introspection RPCs (GetTestStatus, ListTests, GetResults) | ImGuiTest Bridge | Code | πŸ“‹ Planned | IT-01 - Enable clients to poll test results and query execution state | | IT-06 | Implement widget discovery API for AI agents | ImGuiTest Bridge | Code | πŸ“‹ Planned | IT-01 - DiscoverWidgets RPC to enumerate windows, buttons, inputs | -| IT-07 | Add test recording/replay for regression testing | ImGuiTest Bridge | Code | πŸ“‹ Planned | IT-05 - RecordSession/ReplaySession RPCs with JSON test scripts | -| IT-08 | Enhance error reporting with screenshots and state dumps | ImGuiTest Bridge | Code | πŸ“‹ Planned | IT-01 - Capture widget state on failure for debugging | +| IT-07 | Add test recording/replay for regression testing | ImGuiTest Bridge | Code | βœ… Done | IT-05 - RecordSession/ReplaySession RPCs with JSON test scripts | +| IT-08 | Enhance error reporting with screenshots and state dumps | ImGuiTest Bridge | Code | οΏ½ Active | IT-01 - Capture widget state on failure for debugging | +| IT-08a | Adopt shared error envelope across CLI & services | ImGuiTest Bridge | Code | πŸ”„ Active | IT-08 | +| IT-08b | EditorManager diagnostic overlay & logging | ImGuiTest Bridge | UX | πŸ“‹ Planned | IT-08 | | IT-09 | Create standardized test suite format for CI integration | ImGuiTest Bridge | Infra | πŸ“‹ Planned | IT-07 - JSON/YAML test suite format compatible with CI/CD pipelines | | VP-01 | Expand CLI unit tests for new commands and sandbox flow. | Verification Pipeline | Test | πŸ“‹ Planned | RC/AW tasks | | VP-02 | Add harness integration tests with replay scripts. | Verification Pipeline | Test | πŸ“‹ Planned | IT tasks | @@ -495,7 +461,7 @@ _Status Legend: πŸ”„ Active Β· πŸ“‹ Planned Β· βœ… Done_ - πŸ“‹ Next: Test widget discovery and update test harness - See: [WIDGET_ID_REFACTORING_PROGRESS.md](WIDGET_ID_REFACTORING_PROGRESS.md) -### Priority 1: ImGuiTestHarness Foundation (IT-01) βœ… PHASE 2 COMPLETE +### Priority 1: ImGuiTestHarness Foundation (IT-01) βœ… COMPLETE **Rationale**: Required for automated GUI testing and remote control of YAZE for AI workflows **Decision**: βœ… **Use gRPC** - Production-grade, cross-platform, type-safe (see `IT-01-grpc-evaluation.md`) @@ -599,9 +565,10 @@ grpcurl -plaintext -d '{"message":"test"}' \ #### Phase 4: CLI Integration & Windows Testing (4-5 hours) 7. **CLI Client** (`z3ed agent test`) - - Generate gRPC calls from AI prompts - - Natural language β†’ ImGui action translation - - Screenshot capture for LLM feedback + - Generate gRPC calls from AI prompts + - Natural language β†’ ImGui action translation + - Screenshot capture for LLM feedback + - Emit structured error envelopes with artifact links (IT-08) 8. **Windows Testing** - Detailed build instructions for vcpkg setup @@ -992,7 +959,7 @@ A summary of files created or changed during the implementation of the core `z3e **GUI & Application Integration**: - `src/app/editor/system/proposal_drawer.{h,cc}` - `src/app/editor/editor_manager.{h,cc}` -- `src/app/core/imgui_test_harness_service.{h,cc}` +- `src/app/core/service/imgui_test_harness_service.{h,cc}` - `src/app/core/proto/imgui_test_harness.proto` **Build System (CMake)**: @@ -1027,7 +994,7 @@ A summary of files created or changed during the implementation of the core `z3e **Source Code**: - `src/cli/service/` - Core services (proposal registry, sandbox manager, resource catalog) - `src/app/editor/system/proposal_drawer.{h,cc}` - GUI review panel -- `src/app/core/imgui_test_harness_service.{h,cc}` - gRPC automation server +- `src/app/core/service/imgui_test_harness_service.{h,cc}` - gRPC automation server --- diff --git a/docs/z3ed/E6-z3ed-reference.md b/docs/z3ed/E6-z3ed-reference.md index 2c288ff3..4d5c14ea 100644 --- a/docs/z3ed/E6-z3ed-reference.md +++ b/docs/z3ed/E6-z3ed-reference.md @@ -818,7 +818,7 @@ message NewResponse { } ``` -2. **Implement Handler** (`src/app/core/imgui_test_harness_service.cc`) +2. **Implement Handler** (`src/app/core/service/imgui_test_harness_service.cc`) ```cpp grpc::Status ImGuiTestHarnessServiceImpl::NewOperation( grpc::ServerContext* context, diff --git a/docs/z3ed/IT-05-IMPLEMENTATION-GUIDE.md b/docs/z3ed/IT-05-IMPLEMENTATION-GUIDE.md index eff5d59a..379d62e8 100644 --- a/docs/z3ed/IT-05-IMPLEMENTATION-GUIDE.md +++ b/docs/z3ed/IT-05-IMPLEMENTATION-GUIDE.md @@ -236,7 +236,7 @@ message AssertionResult { #### 1.2 Update Existing RPC Handlers -**File**: `src/app/core/imgui_test_harness_service.cc` +**File**: `src/app/core/service/imgui_test_harness_service.cc` Modify Click, Type, Wait, Assert handlers to record test execution: @@ -292,8 +292,8 @@ message ClickResponse { - Ensured deque-backed `DynamicTestData` keep-alive remains bounded while reusing new tracking helpers. **Where to look**: -- `src/app/core/imgui_test_harness_service.cc` (search for `GetTestStatus(`, `ListTests(`, `GetTestResults(`). -- `src/app/core/imgui_test_harness_service.h` (new method declarations). +- `src/app/core/service/imgui_test_harness_service.cc` (search for `GetTestStatus(`, `ListTests(`, `GetTestResults(`). +- `src/app/core/service/imgui_test_harness_service.h` (new method declarations). **Follow-ups**: - Expand `AssertionResult` population once `TestManager` captures structured expected/actual data. @@ -476,7 +476,7 @@ After IT-05 completion: - **Proto Definition**: `src/app/core/proto/imgui_test_harness.proto` - **Test Manager**: `src/app/core/test_manager.{h,cc}` -- **RPC Service**: `src/app/core/imgui_test_harness_service.{h,cc}` +- **RPC Service**: `src/app/core/service/imgui_test_harness_service.{h,cc}` - **CLI Handlers**: `src/cli/handlers/agent.cc` - **Main Plan**: `docs/z3ed/E6-z3ed-implementation-plan.md` diff --git a/docs/z3ed/QUICK_REFERENCE.md b/docs/z3ed/QUICK_REFERENCE.md index 6ed44a57..2c830552 100644 --- a/docs/z3ed/QUICK_REFERENCE.md +++ b/docs/z3ed/QUICK_REFERENCE.md @@ -391,7 +391,7 @@ ls build-grpc-test/_deps/grpc-src/ ``` src/app/core/ β”œβ”€β”€ proto/imgui_test_harness.proto # gRPC service definition - β”œβ”€β”€ imgui_test_harness_service.{h,cc} # RPC implementation + β”œβ”€β”€ core/service/imgui_test_harness_service.{h,cc} # RPC implementation └── test_manager.{h,cc} # Test execution management src/cli/ diff --git a/docs/z3ed/REMOTE_CONTROL_WORKFLOWS.md b/docs/z3ed/REMOTE_CONTROL_WORKFLOWS.md index 9e76e864..468afff5 100644 --- a/docs/z3ed/REMOTE_CONTROL_WORKFLOWS.md +++ b/docs/z3ed/REMOTE_CONTROL_WORKFLOWS.md @@ -201,7 +201,7 @@ z3ed agent discover --pattern "*/button:*" ### Test Harness Changes -**File**: `src/app/core/imgui_test_harness_service.cc` +**File**: `src/app/core/service/imgui_test_harness_service.cc` **Changes**: 1. Added widget registry include @@ -390,7 +390,7 @@ steps: - [E2E_VALIDATION_GUIDE.md](E2E_VALIDATION_GUIDE.md) **Code Files**: -- `src/app/core/imgui_test_harness_service.cc` - Test harness implementation +- `src/app/core/service/imgui_test_harness_service.cc` - Test harness implementation - `src/app/gui/widget_id_registry.{h,cc}` - Widget registry - `src/app/editor/overworld/overworld_editor.cc` - Widget registrations - `scripts/test_remote_control.sh` - Test script diff --git a/src/app/app.cmake b/src/app/app.cmake index 3afe2758..88d33664 100644 --- a/src/app/app.cmake +++ b/src/app/app.cmake @@ -236,14 +236,14 @@ if(YAZE_WITH_GRPC) # Add service implementation sources target_sources(yaze PRIVATE - ${CMAKE_SOURCE_DIR}/src/app/core/imgui_test_harness_service.cc - ${CMAKE_SOURCE_DIR}/src/app/core/imgui_test_harness_service.h - ${CMAKE_SOURCE_DIR}/src/app/core/test_recorder.cc - ${CMAKE_SOURCE_DIR}/src/app/core/test_recorder.h - ${CMAKE_SOURCE_DIR}/src/app/core/test_script_parser.cc - ${CMAKE_SOURCE_DIR}/src/app/core/test_script_parser.h - ${CMAKE_SOURCE_DIR}/src/app/core/widget_discovery_service.cc - ${CMAKE_SOURCE_DIR}/src/app/core/widget_discovery_service.h) + ${CMAKE_SOURCE_DIR}/src/app/core/service/imgui_test_harness_service.cc + ${CMAKE_SOURCE_DIR}/src/app/core/service/imgui_test_harness_service.h + ${CMAKE_SOURCE_DIR}/src/app/core/service/widget_discovery_service.cc + ${CMAKE_SOURCE_DIR}/src/app/core/service/widget_discovery_service.h + ${CMAKE_SOURCE_DIR}/src/app/core/testing/test_recorder.cc + ${CMAKE_SOURCE_DIR}/src/app/core/testing/test_recorder.h + ${CMAKE_SOURCE_DIR}/src/app/core/testing/test_script_parser.cc + ${CMAKE_SOURCE_DIR}/src/app/core/testing/test_script_parser.h) target_include_directories(yaze PRIVATE ${CMAKE_SOURCE_DIR}/third_party/json/include) diff --git a/src/app/core/asar_wrapper.cc b/src/app/core/asar_wrapper.cc index 67205923..3b3a2b99 100644 --- a/src/app/core/asar_wrapper.cc +++ b/src/app/core/asar_wrapper.cc @@ -12,7 +12,6 @@ #include "asar-dll-bindings/c/asar.h" namespace yaze { -namespace app { namespace core { AsarWrapper::AsarWrapper() : initialized_(false) {} @@ -293,5 +292,4 @@ AsarSymbol AsarWrapper::ConvertAsarSymbol(const void* asar_symbol_data) const { } } // namespace core -} // namespace app } // namespace yaze diff --git a/src/app/core/asar_wrapper.h b/src/app/core/asar_wrapper.h index abc810c3..e5de83d4 100644 --- a/src/app/core/asar_wrapper.h +++ b/src/app/core/asar_wrapper.h @@ -11,7 +11,6 @@ #include "absl/status/statusor.h" namespace yaze { -namespace app { namespace core { /** @@ -206,7 +205,6 @@ class AsarWrapper { }; } // namespace core -} // namespace app } // namespace yaze #endif // YAZE_APP_CORE_ASAR_WRAPPER_H diff --git a/src/app/core/proto/imgui_test_harness.proto b/src/app/core/proto/imgui_test_harness.proto index 91333a04..ab80c5ee 100644 --- a/src/app/core/proto/imgui_test_harness.proto +++ b/src/app/core/proto/imgui_test_harness.proto @@ -3,7 +3,7 @@ syntax = "proto3"; package yaze.test; // ImGuiTestHarness service for remote GUI testing -// This service allows z3ed CLI to interact with YAZE's GUI for automated testing +// Allows z3ed CLI to interact with YAZE's GUI for automated testing service ImGuiTestHarness { // Health check - verifies the service is running rpc Ping(PingRequest) returns (PingResponse); diff --git a/src/app/core/imgui_test_harness_service.cc b/src/app/core/service/imgui_test_harness_service.cc similarity index 99% rename from src/app/core/imgui_test_harness_service.cc rename to src/app/core/service/imgui_test_harness_service.cc index f5d2c3dc..2f45203e 100644 --- a/src/app/core/imgui_test_harness_service.cc +++ b/src/app/core/service/imgui_test_harness_service.cc @@ -1,4 +1,4 @@ -#include "app/core/imgui_test_harness_service.h" +#include "app/core/service/imgui_test_harness_service.h" #ifdef YAZE_WITH_GRPC @@ -21,7 +21,7 @@ #include "absl/time/time.h" #include "app/core/proto/imgui_test_harness.grpc.pb.h" #include "app/core/proto/imgui_test_harness.pb.h" -#include "app/core/test_script_parser.h" +#include "app/core/testing/test_script_parser.h" #include "app/test/test_manager.h" #include "yaze.h" // For YAZE_VERSION_STRING diff --git a/src/app/core/imgui_test_harness_service.h b/src/app/core/service/imgui_test_harness_service.h similarity index 94% rename from src/app/core/imgui_test_harness_service.h rename to src/app/core/service/imgui_test_harness_service.h index 95e3b929..a278fa7e 100644 --- a/src/app/core/imgui_test_harness_service.h +++ b/src/app/core/service/imgui_test_harness_service.h @@ -1,5 +1,5 @@ -#ifndef YAZE_APP_CORE_IMGUI_TEST_HARNESS_SERVICE_H_ -#define YAZE_APP_CORE_IMGUI_TEST_HARNESS_SERVICE_H_ +#ifndef YAZE_APP_CORE_SERVICE_IMGUI_TEST_HARNESS_SERVICE_H_ +#define YAZE_APP_CORE_SERVICE_IMGUI_TEST_HARNESS_SERVICE_H_ #ifdef YAZE_WITH_GRPC @@ -8,8 +8,8 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "app/core/test_recorder.h" -#include "app/core/widget_discovery_service.h" +#include "app/core/service/widget_discovery_service.h" +#include "app/core/testing/test_recorder.h" // Include grpcpp headers for unique_ptr in member variable #include @@ -153,4 +153,4 @@ class ImGuiTestHarnessServer { } // namespace yaze #endif // YAZE_WITH_GRPC -#endif // YAZE_APP_CORE_IMGUI_TEST_HARNESS_SERVICE_H_ +#endif // YAZE_APP_CORE_SERVICE_IMGUI_TEST_HARNESS_SERVICE_H_ diff --git a/src/app/core/widget_discovery_service.cc b/src/app/core/service/widget_discovery_service.cc similarity index 99% rename from src/app/core/widget_discovery_service.cc rename to src/app/core/service/widget_discovery_service.cc index bb936db3..f5104c4b 100644 --- a/src/app/core/widget_discovery_service.cc +++ b/src/app/core/service/widget_discovery_service.cc @@ -1,4 +1,4 @@ -#include "app/core/widget_discovery_service.h" +#include "app/core/service/widget_discovery_service.h" #include #include diff --git a/src/app/core/widget_discovery_service.h b/src/app/core/service/widget_discovery_service.h similarity index 88% rename from src/app/core/widget_discovery_service.h rename to src/app/core/service/widget_discovery_service.h index 873a4a21..0b5dc0e6 100644 --- a/src/app/core/widget_discovery_service.h +++ b/src/app/core/service/widget_discovery_service.h @@ -1,5 +1,5 @@ -#ifndef YAZE_APP_CORE_WIDGET_DISCOVERY_SERVICE_H_ -#define YAZE_APP_CORE_WIDGET_DISCOVERY_SERVICE_H_ +#ifndef YAZE_APP_CORE_SERVICE_WIDGET_DISCOVERY_SERVICE_H_ +#define YAZE_APP_CORE_SERVICE_WIDGET_DISCOVERY_SERVICE_H_ #include #include @@ -44,4 +44,4 @@ class WidgetDiscoveryService { } // namespace test } // namespace yaze -#endif // YAZE_APP_CORE_WIDGET_DISCOVERY_SERVICE_H_ +#endif // YAZE_APP_CORE_SERVICE_WIDGET_DISCOVERY_SERVICE_H_ diff --git a/src/app/core/test_recorder.cc b/src/app/core/testing/test_recorder.cc similarity index 98% rename from src/app/core/test_recorder.cc rename to src/app/core/testing/test_recorder.cc index 3a71de41..1b3a53ac 100644 --- a/src/app/core/test_recorder.cc +++ b/src/app/core/testing/test_recorder.cc @@ -1,4 +1,4 @@ -#include "app/core/test_recorder.h" +#include "app/core/testing/test_recorder.h" #include @@ -7,7 +7,7 @@ #include "absl/strings/str_format.h" #include "absl/time/clock.h" #include "absl/time/time.h" -#include "app/core/test_script_parser.h" +#include "app/core/testing/test_script_parser.h" #include "app/test/test_manager.h" namespace yaze { diff --git a/src/app/core/test_recorder.h b/src/app/core/testing/test_recorder.h similarity index 93% rename from src/app/core/test_recorder.h rename to src/app/core/testing/test_recorder.h index 51fb5f0e..f5c073f6 100644 --- a/src/app/core/test_recorder.h +++ b/src/app/core/testing/test_recorder.h @@ -1,5 +1,5 @@ -#ifndef YAZE_APP_CORE_TEST_RECORDER_H_ -#define YAZE_APP_CORE_TEST_RECORDER_H_ +#ifndef YAZE_APP_CORE_TESTING_TEST_RECORDER_H_ +#define YAZE_APP_CORE_TESTING_TEST_RECORDER_H_ #include #include @@ -52,7 +52,7 @@ class TestRecorder { std::vector assertion_failures; std::string expected_value; std::string actual_value; - HarnessTestStatus final_status = HarnessTestStatus::kUnspecified; + HarnessTestStatus final_status = HarnessTestStatus::kUnspecified; std::string final_error_message; std::map metrics; absl::Time captured_at = absl::InfinitePast(); @@ -118,4 +118,4 @@ class TestRecorder { } // namespace test } // namespace yaze -#endif // YAZE_APP_CORE_TEST_RECORDER_H_ +#endif // YAZE_APP_CORE_TESTING_TEST_RECORDER_H_ diff --git a/src/app/core/test_script_parser.cc b/src/app/core/testing/test_script_parser.cc similarity index 99% rename from src/app/core/test_script_parser.cc rename to src/app/core/testing/test_script_parser.cc index 739a5373..2a261833 100644 --- a/src/app/core/test_script_parser.cc +++ b/src/app/core/testing/test_script_parser.cc @@ -1,4 +1,4 @@ -#include "app/core/test_script_parser.h" +#include "app/core/testing/test_script_parser.h" #include #include diff --git a/src/app/core/test_script_parser.h b/src/app/core/testing/test_script_parser.h similarity index 87% rename from src/app/core/test_script_parser.h rename to src/app/core/testing/test_script_parser.h index 3a208e6c..e15dec11 100644 --- a/src/app/core/test_script_parser.h +++ b/src/app/core/testing/test_script_parser.h @@ -1,5 +1,5 @@ -#ifndef YAZE_APP_CORE_TEST_SCRIPT_PARSER_H_ -#define YAZE_APP_CORE_TEST_SCRIPT_PARSER_H_ +#ifndef YAZE_APP_CORE_TESTING_TEST_SCRIPT_PARSER_H_ +#define YAZE_APP_CORE_TESTING_TEST_SCRIPT_PARSER_H_ #include #include @@ -50,4 +50,4 @@ class TestScriptParser { } // namespace test } // namespace yaze -#endif // YAZE_APP_CORE_TEST_SCRIPT_PARSER_H_ +#endif // YAZE_APP_CORE_TESTING_TEST_SCRIPT_PARSER_H_ diff --git a/src/app/editor/overworld/overworld_editor.cc b/src/app/editor/overworld/overworld_editor.cc index 3aa1fdfc..19075b5a 100644 --- a/src/app/editor/overworld/overworld_editor.cc +++ b/src/app/editor/overworld/overworld_editor.cc @@ -3584,7 +3584,7 @@ absl::Status OverworldEditor::ApplyZSCustomOverworldASM(int target_version) { util::logf("Applying ZSCustomOverworld ASM v%d to ROM...", target_version); // Initialize Asar wrapper - auto asar_wrapper = std::make_unique(); + auto asar_wrapper = std::make_unique(); RETURN_IF_ERROR(asar_wrapper->Initialize()); // Create backup of ROM data diff --git a/src/app/main.cc b/src/app/main.cc index 651508dd..cdf35211 100644 --- a/src/app/main.cc +++ b/src/app/main.cc @@ -11,7 +11,7 @@ #include "util/log.h" #ifdef YAZE_WITH_GRPC -#include "app/core/imgui_test_harness_service.h" +#include "app/core/service/imgui_test_harness_service.h" #include "app/test/test_manager.h" #endif diff --git a/src/cli/handlers/agent.cc b/src/cli/handlers/agent.cc index 9057a4ba..55b9e36a 100644 --- a/src/cli/handlers/agent.cc +++ b/src/cli/handlers/agent.cc @@ -1,1582 +1,63 @@ +#include "cli/handlers/agent/commands.h" #include "cli/z3ed.h" -#include "cli/modern_cli.h" -#include "cli/service/ai_service.h" -#include "cli/service/proposal_registry.h" -#include "cli/service/resource_catalog.h" -#include "cli/service/rom_sandbox_manager.h" -#include "cli/service/gui_automation_client.h" -#include "cli/service/test_workflow_generator.h" -#include "util/macro.h" -#include "absl/flags/declare.h" -#include "absl/flags/flag.h" +#include +#include + #include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/ascii.h" -#include "absl/strings/match.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" -#include "absl/strings/str_replace.h" -#include "absl/time/time.h" - -#include // For EXIT_FAILURE -#include -#include -#include - -#include -#include -#include - -// Declare the rom flag so we can access it -ABSL_DECLARE_FLAG(std::string, rom); - -// Platform-specific includes for process management and executable path detection -#if !defined(_WIN32) -#include -#include -#include - -#ifdef __APPLE__ -#include -#endif -#endif namespace yaze { namespace cli { - -// Mock AI service is defined in ai_service.h - +namespace agent { namespace { -struct DescribeOptions { - std::optional resource; - std::string format = "json"; - std::optional output_path; - std::string version = "0.1.0"; - std::optional last_updated; -}; +constexpr absl::string_view kUsage = + "Usage: agent " + "[options]"; -std::string FormatOptionalTime(const std::optional& time) { - if (!time.has_value()) { - return "n/a"; - } - return absl::FormatTime("%Y-%m-%dT%H:%M:%SZ", *time, absl::UTCTimeZone()); -} - -std::string TestRunStatusToString(TestRunStatus status) { - switch (status) { - case TestRunStatus::kQueued: - return "QUEUED"; - case TestRunStatus::kRunning: - return "RUNNING"; - case TestRunStatus::kPassed: - return "PASSED"; - case TestRunStatus::kFailed: - return "FAILED"; - case TestRunStatus::kTimeout: - return "TIMEOUT"; - case TestRunStatus::kUnknown: - default: - return "UNKNOWN"; - } -} - -bool IsTerminalStatus(TestRunStatus status) { - switch (status) { - case TestRunStatus::kQueued: - case TestRunStatus::kRunning: - return false; - case TestRunStatus::kPassed: - case TestRunStatus::kFailed: - case TestRunStatus::kTimeout: - case TestRunStatus::kUnknown: - default: - return true; - } -} - -std::optional ParseStatusFilter(absl::string_view value) { - std::string lower = std::string(absl::AsciiStrToLower(value)); - if (lower == "queued") return TestRunStatus::kQueued; - if (lower == "running") return TestRunStatus::kRunning; - if (lower == "passed") return TestRunStatus::kPassed; - if (lower == "failed") return TestRunStatus::kFailed; - if (lower == "timeout") return TestRunStatus::kTimeout; - if (lower == "unknown") return TestRunStatus::kUnknown; - return std::nullopt; -} - -std::optional ParseWidgetTypeFilter( - absl::string_view value) { - std::string lower = std::string(absl::AsciiStrToLower(value)); - if (lower.empty() || lower == "unspecified" || lower == "any") { - return WidgetTypeFilter::kUnspecified; - } - if (lower == "all") { - return WidgetTypeFilter::kAll; - } - if (lower == "button" || lower == "buttons") { - return WidgetTypeFilter::kButton; - } - if (lower == "input" || lower == "textbox" || lower == "field") { - return WidgetTypeFilter::kInput; - } - if (lower == "menu" || lower == "menuitem" || lower == "menu-item") { - return WidgetTypeFilter::kMenu; - } - if (lower == "tab" || lower == "tabs") { - return WidgetTypeFilter::kTab; - } - if (lower == "checkbox" || lower == "toggle") { - return WidgetTypeFilter::kCheckbox; - } - if (lower == "slider" || lower == "drag" || lower == "sliderfloat") { - return WidgetTypeFilter::kSlider; - } - if (lower == "canvas" || lower == "viewport") { - return WidgetTypeFilter::kCanvas; - } - if (lower == "selectable" || lower == "list-item") { - return WidgetTypeFilter::kSelectable; - } - if (lower == "other") { - return WidgetTypeFilter::kOther; - } - return std::nullopt; -} - -std::string HarnessAddress(const std::string& host, int port) { - return absl::StrFormat("%s:%d", host, port); -} - -std::string JsonEscape(absl::string_view value) { - std::string out; - out.reserve(value.size() + 8); - for (unsigned char c : value) { - switch (c) { - case '\\': - out += "\\\\"; - break; - case '"': - out += "\\\""; - break; - case '\b': - out += "\\b"; - break; - case '\f': - out += "\\f"; - break; - case '\n': - out += "\\n"; - break; - case '\r': - out += "\\r"; - break; - case '\t': - out += "\\t"; - break; - default: - if (c < 0x20) { - absl::StrAppend(&out, absl::StrFormat("\\\\u%04X", static_cast(c))); - } else { - out.push_back(static_cast(c)); - } - } - } - return out; -} - -std::string YamlQuote(absl::string_view value) { - std::string escaped(value); - absl::StrReplaceAll({{"\\", "\\\\"}, {"\"", "\\\""}}, &escaped); - return absl::StrCat("\"", escaped, "\""); -} - -absl::StatusOr ParseDescribeArgs( - const std::vector& args) { - DescribeOptions options; - for (size_t i = 0; i < args.size(); ++i) { - const std::string& token = args[i]; - std::string flag = token; - std::optional inline_value; - - if (absl::StartsWith(token, "--")) { - auto eq_pos = token.find('='); - if (eq_pos != std::string::npos) { - flag = token.substr(0, eq_pos); - inline_value = token.substr(eq_pos + 1); - } - } - - auto require_value = [&](absl::string_view flag_name) -> absl::StatusOr { - if (inline_value.has_value()) { - return *inline_value; - } - if (i + 1 >= args.size()) { - return absl::InvalidArgumentError( - absl::StrFormat("Flag %s requires a value", flag_name)); - } - return args[++i]; - }; - - if (flag == "--resource") { - ASSIGN_OR_RETURN(auto value, require_value("--resource")); - options.resource = std::move(value); - } else if (flag == "--format") { - ASSIGN_OR_RETURN(auto value, require_value("--format")); - options.format = std::move(value); - } else if (flag == "--output") { - ASSIGN_OR_RETURN(auto value, require_value("--output")); - options.output_path = std::move(value); - } else if (flag == "--version") { - ASSIGN_OR_RETURN(auto value, require_value("--version")); - options.version = std::move(value); - } else if (flag == "--last-updated") { - ASSIGN_OR_RETURN(auto value, require_value("--last-updated")); - options.last_updated = std::move(value); - } else { - return absl::InvalidArgumentError( - absl::StrFormat("Unknown flag for agent describe: %s", token)); - } - } - - options.format = absl::AsciiStrToLower(options.format); - if (options.format != "json" && options.format != "yaml") { - return absl::InvalidArgumentError("--format must be either json or yaml"); - } - - return options; -} - -absl::Status HandleRunCommand(const std::vector& arg_vec, Rom& rom) { - if (arg_vec.size() < 2 || arg_vec[0] != "--prompt") { - return absl::InvalidArgumentError("Usage: agent run --prompt "); - } - std::string prompt = arg_vec[1]; - - // Load ROM if not already loaded - if (!rom.is_loaded()) { - std::string rom_path = absl::GetFlag(FLAGS_rom); - if (rom_path.empty()) { - return absl::FailedPreconditionError( - "No ROM loaded. Use --rom= to specify ROM file.\n" - "Example: z3ed agent run --rom=zelda3.sfc --prompt \"Your prompt here\""); - } - - auto status = rom.LoadFromFile(rom_path); - if (!status.ok()) { - return absl::FailedPreconditionError( - absl::StrFormat("Failed to load ROM from '%s': %s", - rom_path, status.message())); - } - } - - auto sandbox_or = RomSandboxManager::Instance().CreateSandbox( - rom, "agent-run"); - if (!sandbox_or.ok()) { - return sandbox_or.status(); - } - auto sandbox = sandbox_or.value(); - - // Create a proposal to track this agent run - auto proposal_or = ProposalRegistry::Instance().CreateProposal( - sandbox.id, prompt, "Agent-generated ROM modifications"); - if (!proposal_or.ok()) { - return proposal_or.status(); - } - auto proposal = proposal_or.value(); - - // Log the start of execution - RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( - proposal.id, absl::StrCat("Starting agent run with prompt: ", prompt))); - - MockAIService ai_service; - auto commands_or = ai_service.GetCommands(prompt); - if (!commands_or.ok()) { - RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( - proposal.id, absl::StrCat("AI service error: ", commands_or.status().message()))); - return commands_or.status(); - } - std::vector commands = commands_or.value(); - - // Log the planned commands - RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( - proposal.id, absl::StrCat("Generated ", commands.size(), " commands"))); - - ModernCLI cli; - int commands_executed = 0; - for (const auto& command : commands) { - RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( - proposal.id, absl::StrCat("Executing: ", command))); - - std::vector command_parts; - std::string current_part; - bool in_quotes = false; - for (char c : command) { - if (c == '\"') { - in_quotes = !in_quotes; - } else if (c == ' ' && !in_quotes) { - command_parts.push_back(current_part); - current_part.clear(); - } else { - current_part += c; - } - } - command_parts.push_back(current_part); - - std::string cmd_name = command_parts[0] + " " + command_parts[1]; - std::vector cmd_args(command_parts.begin() + 2, command_parts.end()); - - auto it = cli.commands_.find(cmd_name); - if (it != cli.commands_.end()) { - auto status = it->second.handler(cmd_args); - if (!status.ok()) { - RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( - proposal.id, absl::StrCat("Command failed: ", status.message()))); - return status; - } - commands_executed++; - RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( - proposal.id, "Command succeeded")); - } else { - auto error_msg = absl::StrCat("Unknown command: ", cmd_name); - RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( - proposal.id, error_msg)); - return absl::NotFoundError(error_msg); - } - } - - // Update proposal with execution stats - RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( - proposal.id, absl::StrCat("Completed execution of ", commands_executed, " commands"))); - - std::cout << "βœ… Agent run completed successfully." << std::endl; - std::cout << " Proposal ID: " << proposal.id << std::endl; - std::cout << " Sandbox: " << sandbox.rom_path << std::endl; - std::cout << " Use 'z3ed agent diff' to review changes" << std::endl; - - return absl::OkStatus(); -} - -absl::Status HandlePlanCommand(const std::vector& arg_vec) { - if (arg_vec.size() < 2 || arg_vec[0] != "--prompt") { - return absl::InvalidArgumentError("Usage: agent plan --prompt "); - } - std::string prompt = arg_vec[1]; - MockAIService ai_service; - auto commands_or = ai_service.GetCommands(prompt); - if (!commands_or.ok()) { - return commands_or.status(); - } - std::vector commands = commands_or.value(); - - std::cout << "AI Agent Plan:" << std::endl; - for (const auto& command : commands) { - std::cout << " - " << command << std::endl; - } - return absl::OkStatus(); -} - -absl::Status HandleDiffCommand(Rom& rom, const std::vector& args) { - // Parse optional --proposal-id flag - std::optional proposal_id; - for (size_t i = 0; i < args.size(); ++i) { - const std::string& token = args[i]; - if (absl::StartsWith(token, "--proposal-id=")) { - proposal_id = token.substr(14); // Length of "--proposal-id=" - } else if (token == "--proposal-id" && i + 1 < args.size()) { - proposal_id = args[i + 1]; - ++i; - } - } - - auto& registry = ProposalRegistry::Instance(); - absl::StatusOr proposal_or; - - // Get specific proposal or latest pending - if (proposal_id.has_value()) { - proposal_or = registry.GetProposal(proposal_id.value()); - } else { - proposal_or = registry.GetLatestPendingProposal(); - } - - if (proposal_or.ok()) { - const auto& proposal = proposal_or.value(); - - std::cout << "\n=== Proposal Diff ===\n"; - std::cout << "Proposal ID: " << proposal.id << "\n"; - std::cout << "Sandbox ID: " << proposal.sandbox_id << "\n"; - std::cout << "Prompt: " << proposal.prompt << "\n"; - std::cout << "Description: " << proposal.description << "\n"; - std::cout << "Status: "; - switch (proposal.status) { - case ProposalRegistry::ProposalStatus::kPending: - std::cout << "Pending"; - break; - case ProposalRegistry::ProposalStatus::kAccepted: - std::cout << "Accepted"; - break; - case ProposalRegistry::ProposalStatus::kRejected: - std::cout << "Rejected"; - break; - } - std::cout << "\n"; - std::cout << "Created: " << absl::FormatTime(proposal.created_at) << "\n"; - std::cout << "Commands Executed: " << proposal.commands_executed << "\n"; - std::cout << "Bytes Changed: " << proposal.bytes_changed << "\n\n"; - - // Read and display the diff file - if (std::filesystem::exists(proposal.diff_path)) { - std::cout << "--- Diff Content ---\n"; - std::ifstream diff_file(proposal.diff_path); - if (diff_file.is_open()) { - std::string line; - while (std::getline(diff_file, line)) { - std::cout << line << "\n"; - } - diff_file.close(); - } else { - std::cout << "(Unable to read diff file)\n"; - } - } else { - std::cout << "(No diff file found)\n"; - } - - // Display execution log summary - std::cout << "\n--- Execution Log ---\n"; - if (std::filesystem::exists(proposal.log_path)) { - std::ifstream log_file(proposal.log_path); - if (log_file.is_open()) { - std::string line; - int line_count = 0; - while (std::getline(log_file, line)) { - std::cout << line << "\n"; - line_count++; - if (line_count > 50) { // Limit output for readability - std::cout << "... (log truncated, see " << proposal.log_path << " for full output)\n"; - break; - } - } - log_file.close(); - } else { - std::cout << "(Unable to read log file)\n"; - } - } else { - std::cout << "(No log file found)\n"; - } - - // Display next steps - std::cout << "\n=== Next Steps ===\n"; - std::cout << "To accept changes: z3ed agent commit\n"; - std::cout << "To reject changes: z3ed agent revert\n"; - std::cout << "To review in GUI: yaze --proposal=" << proposal.id << "\n"; - - return absl::OkStatus(); - } - - // Fallback to old behavior if no proposal found - if (rom.is_loaded()) { - auto sandbox_or = RomSandboxManager::Instance().ActiveSandbox(); - if (!sandbox_or.ok()) { - return absl::NotFoundError( - "No pending proposals found and no active sandbox. " - "Run 'z3ed agent run' first."); - } - RomDiff diff_handler; - auto status = diff_handler.Run( - {rom.filename(), sandbox_or->rom_path.string()}); - if (!status.ok()) { - return status; - } - } else { - return absl::AbortedError("No ROM loaded."); - } - return absl::OkStatus(); -} - -absl::Status HandleTestRunCommand(const std::vector& arg_vec) { - // Parse arguments - std::string prompt; - std::string host = "localhost"; - int port = 50052; - int timeout_sec = 30; - - for (size_t i = 0; i < arg_vec.size(); ++i) { - const std::string& token = arg_vec[i]; - - if (token == "--prompt" && i + 1 < arg_vec.size()) { - prompt = arg_vec[++i]; - } else if (token == "--host" && i + 1 < arg_vec.size()) { - host = arg_vec[++i]; - } else if (token == "--port" && i + 1 < arg_vec.size()) { - port = std::stoi(arg_vec[++i]); - } else if (token == "--timeout" && i + 1 < arg_vec.size()) { - timeout_sec = std::stoi(arg_vec[++i]); - } else if (absl::StartsWith(token, "--prompt=")) { - prompt = token.substr(9); - } else if (absl::StartsWith(token, "--host=")) { - host = token.substr(7); - } else if (absl::StartsWith(token, "--port=")) { - port = std::stoi(token.substr(7)); - } else if (absl::StartsWith(token, "--timeout=")) { - timeout_sec = std::stoi(token.substr(10)); - } - } - - if (prompt.empty()) { - return absl::InvalidArgumentError( - "Usage: agent test --prompt \"\" [--host ] [--port ] [--timeout ]\n\n" - "Examples:\n" - " z3ed agent test --prompt \"Open Overworld editor\"\n" - " z3ed agent test --prompt \"Open Dungeon editor and verify it loads\"\n" - " z3ed agent test --prompt \"Click Open ROM button\""); - } - -#ifndef YAZE_WITH_GRPC - return absl::UnimplementedError( - "GUI automation requires YAZE_WITH_GRPC=ON at build time.\n" - "Rebuild with: cmake -B build -DYAZE_WITH_GRPC=ON"); -#else - std::cout << "\n=== GUI Automation Test ===\n"; - std::cout << "Prompt: " << prompt << "\n"; - std::cout << "Server: " << host << ":" << port << "\n\n"; - - // Generate workflow from prompt - TestWorkflowGenerator generator; - auto workflow_or = generator.GenerateWorkflow(prompt); - if (!workflow_or.ok()) { - return workflow_or.status(); - } - auto workflow = workflow_or.value(); - - std::cout << "Generated workflow:\n" << workflow.ToString() << "\n"; - - // Connect to test harness - GuiAutomationClient client(HarnessAddress(host, port)); - auto connect_status = client.Connect(); - if (!connect_status.ok()) { - return absl::UnavailableError( - absl::StrFormat( - "Failed to connect to test harness at %s:%d\n" - "Make sure YAZE is running with:\n" - " ./yaze --enable_test_harness --test_harness_port=%d --rom_file=\n\n" - "Error: %s", - host, port, port, connect_status.message())); - } - - std::cout << "βœ“ Connected to test harness\n\n"; - - // Execute workflow - auto start_time = std::chrono::steady_clock::now(); - int step_num = 0; - std::vector emitted_test_ids; - - for (const auto& step : workflow.steps) { - step_num++; - std::cout << absl::StrFormat("[%d/%d] %s ... ", step_num, - workflow.steps.size(), step.ToString()); - std::cout.flush(); - - absl::StatusOr result; - - switch (step.type) { - case TestStepType::kClick: - result = client.Click(step.target); - break; - case TestStepType::kType: - result = client.Type(step.target, step.text, step.clear_first); - break; - case TestStepType::kWait: - result = client.Wait(step.condition, step.timeout_ms); - break; - case TestStepType::kAssert: - result = client.Assert(step.condition); - break; - case TestStepType::kScreenshot: - result = client.Screenshot(); - break; - } - - if (!result.ok()) { - std::cout << "βœ— FAILED\n"; - return absl::InternalError( - absl::StrFormat("Step %d failed: %s", step_num, - result.status().message())); - } - - if (!result->success) { - std::cout << "βœ— FAILED\n"; - std::cout << " Error: " << result->message << "\n"; - return absl::InternalError( - absl::StrFormat("Step %d failed: %s", step_num, result->message)); - } - - std::cout << absl::StrFormat("βœ“ (%lldms)", - result->execution_time.count()); - if (!result->test_id.empty()) { - std::cout << " [Test ID: " << result->test_id << "]"; - emitted_test_ids.push_back(result->test_id); - } - std::cout << "\n"; - } - - auto end_time = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration_cast( - end_time - start_time); - - std::cout << "\nβœ… Test passed in " << elapsed.count() << "ms\n"; - - if (!emitted_test_ids.empty()) { - std::cout << "Latest Test ID: " << emitted_test_ids.back() << "\n"; - if (emitted_test_ids.size() > 1) { - std::cout << "Captured Test IDs:\n"; - for (const auto& id : emitted_test_ids) { - std::cout << " - " << id << "\n"; - } - } - std::cout << "Use 'z3ed agent test status --test-id " << emitted_test_ids.back() - << "' for live status updates." << std::endl; - } - - return absl::OkStatus(); -#endif -} - -absl::Status HandleTestStatusCommand(const std::vector& arg_vec) { - std::string host = "localhost"; - int port = 50052; - std::string test_id; - bool follow = false; - int interval_ms = 1000; - - for (size_t i = 0; i < arg_vec.size(); ++i) { - const std::string& token = arg_vec[i]; - - if (token == "--test-id" && i + 1 < arg_vec.size()) { - test_id = arg_vec[++i]; - } else if (absl::StartsWith(token, "--test-id=")) { - test_id = token.substr(10); - } else if (token == "--host" && i + 1 < arg_vec.size()) { - host = arg_vec[++i]; - } else if (absl::StartsWith(token, "--host=")) { - host = token.substr(7); - } else if (token == "--port" && i + 1 < arg_vec.size()) { - port = std::stoi(arg_vec[++i]); - } else if (absl::StartsWith(token, "--port=")) { - port = std::stoi(token.substr(7)); - } else if (token == "--follow") { - follow = true; - } else if ((token == "--interval" || token == "--interval-ms") && - i + 1 < arg_vec.size()) { - interval_ms = std::max(100, std::stoi(arg_vec[++i])); - } else if (absl::StartsWith(token, "--interval=") || - absl::StartsWith(token, "--interval-ms=")) { - size_t prefix = token.find('='); - interval_ms = std::max(100, std::stoi(token.substr(prefix + 1))); - } - } - - if (test_id.empty()) { - return absl::InvalidArgumentError( - "Usage: agent test status --test-id [--follow] [--host ] [--port ] [--interval-ms ]"); - } - -#ifndef YAZE_WITH_GRPC - return absl::UnimplementedError( - "GUI automation requires YAZE_WITH_GRPC=ON at build time.\n" - "Rebuild with: cmake -B build -DYAZE_WITH_GRPC=ON"); -#else - GuiAutomationClient client(HarnessAddress(host, port)); - RETURN_IF_ERROR(client.Connect()); - - std::cout << "\n=== Test Status ===\n"; - std::cout << "Test ID: " << test_id << "\n"; - std::cout << "Server: " << HarnessAddress(host, port) << "\n"; - if (follow) { - std::cout << "Follow mode: polling every " << interval_ms << "ms\n"; - } - std::cout << "\n"; - - bool first_iteration = true; - while (true) { - ASSIGN_OR_RETURN(auto details, client.GetTestStatus(test_id)); - - if (!first_iteration) { - std::cout << "---\n"; - } - - std::cout << "Status: " << TestRunStatusToString(details.status) << "\n"; - std::cout << "Queued At: " << FormatOptionalTime(details.queued_at) << "\n"; - std::cout << "Started At: " << FormatOptionalTime(details.started_at) << "\n"; - std::cout << "Completed At: " << FormatOptionalTime(details.completed_at) << "\n"; - std::cout << "Execution Time (ms): " << details.execution_time_ms << "\n"; - if (!details.error_message.empty()) { - std::cout << "Error: " << details.error_message << "\n"; - } - - if (!details.assertion_failures.empty()) { - std::cout << "Assertion Failures (" << details.assertion_failures.size() - << "):\n"; - for (const auto& failure : details.assertion_failures) { - std::cout << " - " << failure << "\n"; - } - } else { - std::cout << "Assertion Failures: 0\n"; - } - - if (!follow || IsTerminalStatus(details.status)) { - break; - } - - first_iteration = false; - std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms)); - } - - return absl::OkStatus(); -#endif -} - -absl::Status HandleTestListCommand(const std::vector& arg_vec) { - std::string host = "localhost"; - int port = 50052; - std::string category_filter; - std::optional status_filter; - int page_size = 100; - int limit = -1; - bool fetch_all = false; - - for (size_t i = 0; i < arg_vec.size(); ++i) { - const std::string& token = arg_vec[i]; - - if (token == "--host" && i + 1 < arg_vec.size()) { - host = arg_vec[++i]; - } else if (absl::StartsWith(token, "--host=")) { - host = token.substr(7); - } else if (token == "--port" && i + 1 < arg_vec.size()) { - port = std::stoi(arg_vec[++i]); - } else if (absl::StartsWith(token, "--port=")) { - port = std::stoi(token.substr(7)); - } else if (token == "--category" && i + 1 < arg_vec.size()) { - category_filter = arg_vec[++i]; - } else if (absl::StartsWith(token, "--category=")) { - category_filter = token.substr(11); - } else if (token == "--status" && i + 1 < arg_vec.size()) { - auto parsed = ParseStatusFilter(arg_vec[++i]); - if (!parsed.has_value()) { - return absl::InvalidArgumentError( - "Invalid status filter. Expected: queued, running, passed, failed, timeout, unknown"); - } - status_filter = parsed; - } else if (absl::StartsWith(token, "--status=")) { - auto parsed = ParseStatusFilter(token.substr(9)); - if (!parsed.has_value()) { - return absl::InvalidArgumentError( - "Invalid status filter. Expected: queued, running, passed, failed, timeout, unknown"); - } - status_filter = parsed; - } else if (token == "--page-size" && i + 1 < arg_vec.size()) { - page_size = std::max(1, std::stoi(arg_vec[++i])); - } else if (absl::StartsWith(token, "--page-size=")) { - page_size = std::max(1, std::stoi(token.substr(12))); - } else if (token == "--limit" && i + 1 < arg_vec.size()) { - limit = std::stoi(arg_vec[++i]); - } else if (absl::StartsWith(token, "--limit=")) { - limit = std::stoi(token.substr(8)); - } else if (token == "--all") { - fetch_all = true; - } - } - - if (fetch_all) { - limit = -1; - } - -#ifndef YAZE_WITH_GRPC - return absl::UnimplementedError( - "GUI automation requires YAZE_WITH_GRPC=ON at build time.\n" - "Rebuild with: cmake -B build -DYAZE_WITH_GRPC=ON"); -#else - GuiAutomationClient client(HarnessAddress(host, port)); - RETURN_IF_ERROR(client.Connect()); - - std::cout << "\n=== Harness Test Catalog ===\n"; - std::cout << "Server: " << HarnessAddress(host, port) << "\n"; - if (!category_filter.empty()) { - std::cout << "Category filter: " << category_filter << "\n"; - } - if (status_filter.has_value()) { - std::cout << "Status filter: " - << TestRunStatusToString(status_filter.value()) << "\n"; - } - std::cout << "\n"; - - std::vector collected; - collected.reserve(limit > 0 ? limit : page_size); - std::string page_token; - int total_count = 0; - - while (true) { - int request_page_size = page_size > 0 ? page_size : 100; - if (limit > 0) { - int remaining = limit - static_cast(collected.size()); - if (remaining <= 0) { - break; - } - request_page_size = std::min(request_page_size, remaining); - } - - ASSIGN_OR_RETURN(auto batch, - client.ListTests(category_filter, request_page_size, - page_token)); - - total_count = batch.total_count; - - for (const auto& summary : batch.tests) { - if (status_filter.has_value()) { - ASSIGN_OR_RETURN(auto details, - client.GetTestStatus(summary.test_id)); - if (details.status != status_filter.value()) { - continue; - } - } - - collected.push_back(summary); - if (limit > 0 && static_cast(collected.size()) >= limit) { - break; - } - } - - if (limit > 0 && static_cast(collected.size()) >= limit) { - break; - } - - if (batch.next_page_token.empty()) { - break; - } - page_token = batch.next_page_token; - } - - if (collected.empty()) { - std::cout << "No tests found for the specified filters." << std::endl; - return absl::OkStatus(); - } - - for (const auto& summary : collected) { - std::cout << "Test ID: " << summary.test_id << "\n"; - std::cout << " Name: " << summary.name << "\n"; - std::cout << " Category: " << summary.category << "\n"; - std::cout << " Last Run: " << FormatOptionalTime(summary.last_run_at) - << "\n"; - std::cout << " Runs: " << summary.total_runs << " (" - << summary.pass_count << " pass / " << summary.fail_count - << " fail)\n"; - std::cout << " Average Duration (ms): " << summary.average_duration_ms - << "\n\n"; - } - - std::cout << "Displayed " << collected.size() << " test(s)"; - if (total_count > 0) { - std::cout << " (catalog size: " << total_count << ")"; - } - std::cout << "." << std::endl; - - return absl::OkStatus(); -#endif -} - -absl::Status HandleTestResultsCommand(const std::vector& arg_vec) { - std::string host = "localhost"; - int port = 50052; - std::string test_id; - bool include_logs = false; - std::string format = "yaml"; - - for (size_t i = 0; i < arg_vec.size(); ++i) { - const std::string& token = arg_vec[i]; - - if (token == "--test-id" && i + 1 < arg_vec.size()) { - test_id = arg_vec[++i]; - } else if (absl::StartsWith(token, "--test-id=")) { - test_id = token.substr(10); - } else if (token == "--host" && i + 1 < arg_vec.size()) { - host = arg_vec[++i]; - } else if (absl::StartsWith(token, "--host=")) { - host = token.substr(7); - } else if (token == "--port" && i + 1 < arg_vec.size()) { - port = std::stoi(arg_vec[++i]); - } else if (absl::StartsWith(token, "--port=")) { - port = std::stoi(token.substr(7)); - } else if (token == "--include-logs") { - include_logs = true; - } else if (token == "--format" && i + 1 < arg_vec.size()) { - format = absl::AsciiStrToLower(arg_vec[++i]); - } else if (absl::StartsWith(token, "--format=")) { - format = absl::AsciiStrToLower(token.substr(9)); - } - } - - if (test_id.empty()) { - return absl::InvalidArgumentError( - "Usage: agent test results --test-id [--include-logs] [--format yaml|json] [--host ] [--port ]"); - } - - if (format != "yaml" && format != "json") { - return absl::InvalidArgumentError("--format must be either 'yaml' or 'json'"); - } - -#ifndef YAZE_WITH_GRPC - return absl::UnimplementedError( - "GUI automation requires YAZE_WITH_GRPC=ON at build time.\n" - "Rebuild with: cmake -B build -DYAZE_WITH_GRPC=ON"); -#else - GuiAutomationClient client(HarnessAddress(host, port)); - RETURN_IF_ERROR(client.Connect()); - - ASSIGN_OR_RETURN(auto details, - client.GetTestResults(test_id, include_logs)); - - if (format == "json") { - std::cout << "{\n"; - std::cout << " \"test_id\": \"" << JsonEscape(details.test_id) - << "\",\n"; - std::cout << " \"success\": " << (details.success ? "true" : "false") - << ",\n"; - std::cout << " \"name\": \"" << JsonEscape(details.test_name) - << "\",\n"; - std::cout << " \"category\": \"" << JsonEscape(details.category) - << "\",\n"; - std::cout << " \"executed_at\": \"" - << JsonEscape(FormatOptionalTime(details.executed_at)) - << "\",\n"; - std::cout << " \"duration_ms\": " << details.duration_ms << ",\n"; - - std::cout << " \"assertions\": "; - if (details.assertions.empty()) { - std::cout << "[],\n"; - } else { - std::cout << "[\n"; - for (size_t i = 0; i < details.assertions.size(); ++i) { - const auto& assertion = details.assertions[i]; - std::cout << " {\"description\": \"" - << JsonEscape(assertion.description) - << "\", \"passed\": " - << (assertion.passed ? "true" : "false"); - if (!assertion.expected_value.empty()) { - std::cout << ", \"expected\": \"" - << JsonEscape(assertion.expected_value) << "\""; - } - if (!assertion.actual_value.empty()) { - std::cout << ", \"actual\": \"" - << JsonEscape(assertion.actual_value) << "\""; - } - if (!assertion.error_message.empty()) { - std::cout << ", \"error\": \"" - << JsonEscape(assertion.error_message) << "\""; - } - std::cout << "}"; - if (i + 1 < details.assertions.size()) { - std::cout << ","; - } - std::cout << "\n"; - } - std::cout << " ],\n"; - } - - std::cout << " \"logs\": "; - if (include_logs && !details.logs.empty()) { - std::cout << "[\n"; - for (size_t i = 0; i < details.logs.size(); ++i) { - std::cout << " \"" << JsonEscape(details.logs[i]) << "\""; - if (i + 1 < details.logs.size()) { - std::cout << ","; - } - std::cout << "\n"; - } - std::cout << " ],\n"; - } else { - std::cout << "[],\n"; - } - - std::cout << " \"metrics\": "; - if (!details.metrics.empty()) { - std::cout << "{\n"; - size_t index = 0; - for (const auto& [key, value] : details.metrics) { - std::cout << " \"" << JsonEscape(key) << "\": " << value; - if (index + 1 < details.metrics.size()) { - std::cout << ","; - } - std::cout << "\n"; - ++index; - } - std::cout << " }\n"; - } else { - std::cout << "{}\n"; - } - - std::cout << "}" << std::endl; - } else { - std::cout << "test_id: " << details.test_id << "\n"; - std::cout << "success: " << (details.success ? "true" : "false") - << "\n"; - std::cout << "name: " << YamlQuote(details.test_name) << "\n"; - std::cout << "category: " << YamlQuote(details.category) << "\n"; - std::cout << "executed_at: " << FormatOptionalTime(details.executed_at) - << "\n"; - std::cout << "duration_ms: " << details.duration_ms << "\n"; - - if (details.assertions.empty()) { - std::cout << "assertions: []\n"; - } else { - std::cout << "assertions:\n"; - for (const auto& assertion : details.assertions) { - std::cout << " - description: " - << YamlQuote(assertion.description) << "\n"; - std::cout << " passed: " - << (assertion.passed ? "true" : "false") << "\n"; - if (!assertion.expected_value.empty()) { - std::cout << " expected: " - << YamlQuote(assertion.expected_value) << "\n"; - } - if (!assertion.actual_value.empty()) { - std::cout << " actual: " - << YamlQuote(assertion.actual_value) << "\n"; - } - if (!assertion.error_message.empty()) { - std::cout << " error: " - << YamlQuote(assertion.error_message) << "\n"; - } - } - } - - if (include_logs && !details.logs.empty()) { - std::cout << "logs:\n"; - for (const auto& log : details.logs) { - std::cout << " - " << YamlQuote(log) << "\n"; - } - } else { - std::cout << "logs: []\n"; - } - - if (details.metrics.empty()) { - std::cout << "metrics: {}\n"; - } else { - std::cout << "metrics:\n"; - for (const auto& [key, value] : details.metrics) { - std::cout << " " << key << ": " << value << "\n"; - } - } - } - - return absl::OkStatus(); -#endif -} - -absl::Status HandleTestCommand(const std::vector& arg_vec) { - if (!arg_vec.empty()) { - const std::string& subcommand = arg_vec[0]; - std::vector tail(arg_vec.begin() + 1, arg_vec.end()); - - if (subcommand == "status") { - return HandleTestStatusCommand(tail); - } - if (subcommand == "list") { - return HandleTestListCommand(tail); - } - if (subcommand == "results") { - return HandleTestResultsCommand(tail); - } - } - - return HandleTestRunCommand(arg_vec); -} - -absl::Status HandleGuiDiscoverCommand(const std::vector& arg_vec) { - std::string host = "localhost"; - int port = 50052; - std::string window_filter; - std::string path_prefix; - std::optional type_filter; - std::optional type_filter_label; - bool include_invisible = false; - bool include_disabled = false; - std::string format = "table"; - int limit = -1; - - auto require_value = [&](const std::vector& args, size_t& index, - absl::string_view flag) -> absl::StatusOr { - if (index + 1 >= args.size()) { - return absl::InvalidArgumentError( - absl::StrFormat("Flag %s requires a value", flag)); - } - return args[++index]; - }; - - for (size_t i = 0; i < arg_vec.size(); ++i) { - const std::string& token = arg_vec[i]; - if (token == "--host") { - ASSIGN_OR_RETURN(auto value, require_value(arg_vec, i, "--host")); - host = std::move(value); - } else if (absl::StartsWith(token, "--host=")) { - host = token.substr(7); - } else if (token == "--port") { - ASSIGN_OR_RETURN(auto value, require_value(arg_vec, i, "--port")); - port = std::stoi(value); - } else if (absl::StartsWith(token, "--port=")) { - port = std::stoi(token.substr(7)); - } else if (token == "--window" || token == "--window-filter") { - ASSIGN_OR_RETURN(auto value, - require_value(arg_vec, i, token.c_str())); - window_filter = std::move(value); - } else if (absl::StartsWith(token, "--window=")) { - window_filter = token.substr(9); - } else if (token == "--path-prefix") { - ASSIGN_OR_RETURN(auto value, - require_value(arg_vec, i, "--path-prefix")); - path_prefix = std::move(value); - } else if (absl::StartsWith(token, "--path-prefix=")) { - path_prefix = token.substr(14); - } else if (token == "--type") { - ASSIGN_OR_RETURN(auto value, - require_value(arg_vec, i, "--type")); - auto parsed = ParseWidgetTypeFilter(value); - if (!parsed.has_value()) { - return absl::InvalidArgumentError( - absl::StrFormat("Unknown widget type filter: %s", value)); - } - type_filter = parsed; - type_filter_label = absl::AsciiStrToLower(value); - } else if (absl::StartsWith(token, "--type=")) { - std::string value = token.substr(7); - auto parsed = ParseWidgetTypeFilter(value); - if (!parsed.has_value()) { - return absl::InvalidArgumentError( - absl::StrFormat("Unknown widget type filter: %s", value)); - } - type_filter = parsed; - type_filter_label = absl::AsciiStrToLower(value); - } else if (token == "--include-invisible") { - include_invisible = true; - } else if (token == "--include-disabled") { - include_disabled = true; - } else if (token == "--format") { - ASSIGN_OR_RETURN(auto value, - require_value(arg_vec, i, "--format")); - format = std::move(value); - } else if (absl::StartsWith(token, "--format=")) { - format = token.substr(9); - } else if (token == "--limit") { - ASSIGN_OR_RETURN(auto value, - require_value(arg_vec, i, "--limit")); - limit = std::stoi(value); - } else if (absl::StartsWith(token, "--limit=")) { - limit = std::stoi(token.substr(8)); - } else if (token == "--help" || token == "-h") { - std::cout << "Usage: agent gui discover [options]\n" - << " --host \n" - << " --port \n" - << " --window \n" - << " --type \n" - << " --path-prefix \n" - << " --include-invisible\n" - << " --include-disabled\n" - << " --format \n" - << " --limit \n"; - return absl::OkStatus(); - } else { - return absl::InvalidArgumentError( - absl::StrFormat("Unknown flag for agent gui discover: %s", - token)); - } - } - - format = absl::AsciiStrToLower(format); - if (format != "table" && format != "json") { - return absl::InvalidArgumentError( - "--format must be either 'table' or 'json'"); - } - - if (limit == 0) { - return absl::InvalidArgumentError("--limit must be positive"); - } - -#ifndef YAZE_WITH_GRPC - (void)host; - (void)port; - (void)window_filter; - (void)path_prefix; - (void)type_filter; - (void)include_invisible; - (void)include_disabled; - (void)format; - (void)limit; - return absl::UnimplementedError( - "GUI automation requires YAZE_WITH_GRPC=ON at build time.\n" - "Rebuild with: cmake -B build -DYAZE_WITH_GRPC=ON"); -#else - GuiAutomationClient client(HarnessAddress(host, port)); - RETURN_IF_ERROR(client.Connect()); - - DiscoverWidgetsQuery query; - query.window_filter = window_filter; - query.path_prefix = path_prefix; - if (type_filter.has_value()) { - query.type_filter = type_filter.value(); - } - query.include_invisible = include_invisible; - query.include_disabled = include_disabled; - - ASSIGN_OR_RETURN(auto response, client.DiscoverWidgets(query)); - - int max_items = limit > 0 ? limit : std::numeric_limits::max(); - int remaining = max_items; - std::vector trimmed_windows; - trimmed_windows.reserve(response.windows.size()); - int rendered_widgets = 0; - - for (const auto& window : response.windows) { - if (remaining <= 0) { - break; - } - DiscoveredWindowInfo trimmed; - trimmed.name = window.name; - trimmed.visible = window.visible; - - for (const auto& widget : window.widgets) { - if (remaining <= 0) { - break; - } - trimmed.widgets.push_back(widget); - --remaining; - ++rendered_widgets; - } - - if (!trimmed.widgets.empty()) { - trimmed_windows.push_back(std::move(trimmed)); - } - } - - bool truncated = rendered_widgets < response.total_widgets; - - if (format == "json") { - std::cout << "{\n"; - std::cout << " \"server\": \"" - << JsonEscape(HarnessAddress(host, port)) << "\",\n"; - std::cout << " \"totalWidgets\": " << response.total_widgets << ",\n"; - std::cout << " \"returnedWidgets\": " << rendered_widgets << ",\n"; - std::cout << " \"truncated\": " << (truncated ? "true" : "false") - << ",\n"; - std::cout << " \"generatedAt\": " - << (response.generated_at.has_value() - ? absl::StrCat("\"", - JsonEscape(absl::FormatTime( - "%Y-%m-%dT%H:%M:%SZ", - *response.generated_at, - absl::UTCTimeZone())), - "\"") - : std::string("null")) - << ",\n"; - std::cout << " \"windows\": [\n"; - - for (size_t w = 0; w < trimmed_windows.size(); ++w) { - const auto& window = trimmed_windows[w]; - std::cout << " {\n"; - std::cout << " \"name\": \"" << JsonEscape(window.name) - << "\",\n"; - std::cout << " \"visible\": " - << (window.visible ? "true" : "false") << ",\n"; - std::cout << " \"widgets\": [\n"; - for (size_t i = 0; i < window.widgets.size(); ++i) { - const auto& widget = window.widgets[i]; - std::cout << " {\n"; - std::cout << " \"path\": \"" - << JsonEscape(widget.path) << "\",\n"; - std::cout << " \"label\": \"" - << JsonEscape(widget.label) << "\",\n"; - std::cout << " \"type\": \"" - << JsonEscape(widget.type) << "\",\n"; - std::cout << " \"description\": \"" - << JsonEscape(widget.description) << "\",\n"; - std::cout << " \"suggestedAction\": \"" - << JsonEscape(widget.suggested_action) << "\",\n"; - std::cout << " \"visible\": " - << (widget.visible ? "true" : "false") << ",\n"; - std::cout << " \"enabled\": " - << (widget.enabled ? "true" : "false") << ",\n"; - std::cout << " \"bounds\": { \"min\": [" - << widget.bounds.min_x << ", " - << widget.bounds.min_y << "], \"max\": [" - << widget.bounds.max_x << ", " - << widget.bounds.max_y << "] },\n"; - std::cout << " \"widgetId\": " << widget.widget_id - << "\n"; - std::cout << " }"; - if (i + 1 < window.widgets.size()) { - std::cout << ","; - } - std::cout << "\n"; - } - std::cout << " ]\n"; - std::cout << " }"; - if (w + 1 < trimmed_windows.size()) { - std::cout << ","; - } - std::cout << "\n"; - } - - std::cout << " ]\n"; - std::cout << "}\n"; - return absl::OkStatus(); - } - - std::cout << "\n=== Widget Discovery ===\n"; - std::cout << "Server: " << HarnessAddress(host, port) << "\n"; - if (!window_filter.empty()) { - std::cout << "Window filter: " << window_filter << "\n"; - } - if (!path_prefix.empty()) { - std::cout << "Path prefix: " << path_prefix << "\n"; - } - if (type_filter_label.has_value()) { - std::cout << "Type filter: " << *type_filter_label << "\n"; - } - std::cout << "Include invisible: " << (include_invisible ? "yes" : "no") - << "\n"; - std::cout << "Include disabled: " << (include_disabled ? "yes" : "no") - << "\n\n"; - - if (trimmed_windows.empty()) { - std::cout << "No widgets matched the provided filters." << std::endl; - return absl::OkStatus(); - } - - for (const auto& window : trimmed_windows) { - std::cout << "Window: " << window.name - << (window.visible ? " (visible)" : " (hidden)") - << "\n"; - for (const auto& widget : window.widgets) { - std::cout << " β€’ [" << widget.type << "] " << widget.label - << "\n"; - std::cout << " Path: " << widget.path << "\n"; - if (!widget.description.empty()) { - std::cout << " Description: " << widget.description - << "\n"; - } - std::cout << " Suggested: " << widget.suggested_action << "\n"; - std::cout << " State: " - << (widget.visible ? "visible" : "hidden") << ", " - << (widget.enabled ? "enabled" : "disabled") << "\n"; - std::cout << absl::StrFormat( - " Bounds: (%.1f, %.1f) β†’ (%.1f, %.1f)\n", - widget.bounds.min_x, widget.bounds.min_y, - widget.bounds.max_x, widget.bounds.max_y); - std::cout << " Widget ID: 0x" << std::hex << widget.widget_id - << std::dec << "\n"; - } - std::cout << "\n"; - } - - std::cout << "Widgets shown: " << rendered_widgets << " of " - << response.total_widgets; - if (truncated) { - std::cout << " (truncated)"; - } - std::cout << "\n"; - - if (response.generated_at.has_value()) { - std::cout << "Snapshot: " - << absl::FormatTime("%Y-%m-%d %H:%M:%S", - *response.generated_at, - absl::LocalTimeZone()) - << "\n"; - } - - return absl::OkStatus(); -#endif -} - -absl::Status HandleGuiCommand(const std::vector& arg_vec) { - if (arg_vec.empty()) { - return absl::InvalidArgumentError( - "Usage: agent gui [options]"); - } - - const std::string& subcommand = arg_vec[0]; - std::vector tail(arg_vec.begin() + 1, arg_vec.end()); - - if (subcommand == "discover") { - return HandleGuiDiscoverCommand(tail); - } - - return absl::InvalidArgumentError( - absl::StrFormat("Unknown agent gui subcommand: %s", subcommand)); -} - -absl::Status HandleLearnCommand() { - std::cout << "Agent learn not yet implemented." << std::endl; - return absl::OkStatus(); -} - -absl::Status HandleListCommand() { - auto& registry = ProposalRegistry::Instance(); - auto proposals = registry.ListProposals(); - - if (proposals.empty()) { - std::cout << "No proposals found.\n"; - std::cout << "Run 'z3ed agent run --prompt \"...\"' to create a proposal.\n"; - return absl::OkStatus(); - } - - std::cout << "\n=== Agent Proposals ===\n\n"; - - for (const auto& proposal : proposals) { - std::cout << "ID: " << proposal.id << "\n"; - std::cout << " Status: "; - switch (proposal.status) { - case ProposalRegistry::ProposalStatus::kPending: - std::cout << "Pending"; - break; - case ProposalRegistry::ProposalStatus::kAccepted: - std::cout << "Accepted"; - break; - case ProposalRegistry::ProposalStatus::kRejected: - std::cout << "Rejected"; - break; - } - std::cout << "\n"; - std::cout << " Created: " << absl::FormatTime(proposal.created_at) << "\n"; - std::cout << " Prompt: " << proposal.prompt << "\n"; - std::cout << " Commands: " << proposal.commands_executed << "\n"; - std::cout << " Bytes Changed: " << proposal.bytes_changed << "\n"; - std::cout << "\n"; - } - - std::cout << "Total: " << proposals.size() << " proposal(s)\n"; - std::cout << "\nUse 'z3ed agent diff --proposal-id=' to view details.\n"; - - return absl::OkStatus(); -} - -absl::Status HandleCommitCommand(Rom& rom) { - if (rom.is_loaded()) { - auto status = rom.SaveToFile({.save_new = false}); - if (!status.ok()) { - return status; - } - std::cout << "βœ… Changes committed successfully." << std::endl; - } else { - return absl::AbortedError("No ROM loaded."); - } - return absl::OkStatus(); -} - -absl::Status HandleRevertCommand(Rom& rom) { - if (rom.is_loaded()) { - auto status = rom.LoadFromFile(rom.filename()); - if (!status.ok()) { - return status; - } - std::cout << "βœ… Changes reverted successfully." << std::endl; - } else { - return absl::AbortedError("No ROM loaded."); - } - return absl::OkStatus(); -} - -absl::Status HandleDescribeCommand(const std::vector& arg_vec) { - ASSIGN_OR_RETURN(auto options, ParseDescribeArgs(arg_vec)); - - const auto& catalog = ResourceCatalog::Instance(); - std::optional resource_schema; - if (options.resource.has_value()) { - auto resource_or = catalog.GetResource(*options.resource); - if (!resource_or.ok()) { - return resource_or.status(); - } - resource_schema = resource_or.value(); - } - - std::string payload; - if (options.format == "json") { - if (resource_schema.has_value()) { - payload = catalog.SerializeResource(*resource_schema); - } else { - payload = catalog.SerializeResources(catalog.AllResources()); - } - } else { - std::string last_updated = options.last_updated.has_value() - ? *options.last_updated - : absl::FormatTime("%Y-%m-%d", absl::Now(), - absl::LocalTimeZone()); - if (resource_schema.has_value()) { - std::vector schemas{*resource_schema}; - payload = catalog.SerializeResourcesAsYaml( - schemas, options.version, last_updated); - } else { - payload = catalog.SerializeResourcesAsYaml( - catalog.AllResources(), options.version, last_updated); - } - } - - if (options.output_path.has_value()) { - std::ofstream out(*options.output_path, std::ios::binary | std::ios::trunc); - if (!out.is_open()) { - return absl::InternalError(absl::StrFormat( - "Failed to open %s for writing", *options.output_path)); - } - out << payload; - out.close(); - if (!out) { - return absl::InternalError(absl::StrFormat( - "Failed to write schema to %s", *options.output_path)); - } - std::cout << absl::StrFormat("Wrote %s schema to %s", options.format, - *options.output_path) - << std::endl; - return absl::OkStatus(); - } - - std::cout << payload << std::endl; - return absl::OkStatus(); -} - -} // namespace +} // namespace +} // namespace agent absl::Status Agent::Run(const std::vector& arg_vec) { if (arg_vec.empty()) { - return absl::InvalidArgumentError( - "Usage: agent [options]"); + return absl::InvalidArgumentError(std::string(agent::kUsage)); } - std::string subcommand = arg_vec[0]; + const std::string& subcommand = arg_vec[0]; std::vector subcommand_args(arg_vec.begin() + 1, arg_vec.end()); if (subcommand == "run") { - return HandleRunCommand(subcommand_args, rom_); - } else if (subcommand == "plan") { - return HandlePlanCommand(subcommand_args); - } else if (subcommand == "diff") { - return HandleDiffCommand(rom_, subcommand_args); - } else if (subcommand == "test") { - return HandleTestCommand(subcommand_args); - } else if (subcommand == "gui") { - return HandleGuiCommand(subcommand_args); - } else if (subcommand == "learn") { - return HandleLearnCommand(); - } else if (subcommand == "list") { - return HandleListCommand(); - } else if (subcommand == "commit") { - return HandleCommitCommand(rom_); - } else if (subcommand == "revert") { - return HandleRevertCommand(rom_); - } else if (subcommand == "describe") { - return HandleDescribeCommand(subcommand_args); - } else { - return absl::InvalidArgumentError("Invalid subcommand for agent command."); + return agent::HandleRunCommand(subcommand_args, rom_); + } + if (subcommand == "plan") { + return agent::HandlePlanCommand(subcommand_args); + } + if (subcommand == "diff") { + return agent::HandleDiffCommand(rom_, subcommand_args); + } + if (subcommand == "test") { + return agent::HandleTestCommand(subcommand_args); + } + if (subcommand == "gui") { + return agent::HandleGuiCommand(subcommand_args); + } + if (subcommand == "learn") { + return agent::HandleLearnCommand(); + } + if (subcommand == "list") { + return agent::HandleListCommand(); + } + if (subcommand == "commit") { + return agent::HandleCommitCommand(rom_); + } + if (subcommand == "revert") { + return agent::HandleRevertCommand(rom_); + } + if (subcommand == "describe") { + return agent::HandleDescribeCommand(subcommand_args); } - return absl::OkStatus(); + return absl::InvalidArgumentError(std::string(agent::kUsage)); } } // namespace cli diff --git a/src/cli/handlers/agent/commands.h b/src/cli/handlers/agent/commands.h new file mode 100644 index 00000000..4ea01310 --- /dev/null +++ b/src/cli/handlers/agent/commands.h @@ -0,0 +1,32 @@ +#ifndef YAZE_CLI_HANDLERS_AGENT_COMMANDS_H_ +#define YAZE_CLI_HANDLERS_AGENT_COMMANDS_H_ + +#include +#include + +#include "absl/status/status.h" + +namespace yaze { +class Rom; + +namespace cli { +namespace agent { + +absl::Status HandleRunCommand(const std::vector& args, + Rom& rom); +absl::Status HandlePlanCommand(const std::vector& args); +absl::Status HandleDiffCommand(Rom& rom, + const std::vector& args); +absl::Status HandleTestCommand(const std::vector& args); +absl::Status HandleGuiCommand(const std::vector& args); +absl::Status HandleLearnCommand(); +absl::Status HandleListCommand(); +absl::Status HandleCommitCommand(Rom& rom); +absl::Status HandleRevertCommand(Rom& rom); +absl::Status HandleDescribeCommand(const std::vector& args); + +} // namespace agent +} // namespace cli +} // namespace yaze + +#endif // YAZE_CLI_HANDLERS_AGENT_COMMANDS_H_ diff --git a/src/cli/handlers/agent/common.cc b/src/cli/handlers/agent/common.cc new file mode 100644 index 00000000..aaef9072 --- /dev/null +++ b/src/cli/handlers/agent/common.cc @@ -0,0 +1,172 @@ +#include "cli/handlers/agent/common.h" + +#include "absl/strings/ascii.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_replace.h" + +namespace yaze { +namespace cli { +namespace agent { + +std::string HarnessAddress(const std::string& host, int port) { + return absl::StrFormat("%s:%d", host, port); +} + +std::string JsonEscape(absl::string_view value) { + std::string out; + out.reserve(value.size() + 8); + for (unsigned char c : value) { + switch (c) { + case '\\': + out += "\\\\"; + break; + case '"': + out += "\\\""; + break; + case '\b': + out += "\\b"; + break; + case '\f': + out += "\\f"; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + default: + if (c < 0x20) { + absl::StrAppend(&out, absl::StrFormat("\\u%04X", static_cast(c))); + } else { + out.push_back(static_cast(c)); + } + } + } + return out; +} + +std::string YamlQuote(absl::string_view value) { + std::string escaped(value); + absl::StrReplaceAll({{"\\", "\\\\"}, {"\"", "\\\""}}, &escaped); + return absl::StrCat("\"", escaped, "\""); +} + +std::string FormatOptionalTime(const std::optional& time) { + if (!time.has_value()) { + return "n/a"; + } + return absl::FormatTime("%Y-%m-%dT%H:%M:%SZ", *time, absl::UTCTimeZone()); +} + +std::string OptionalTimeToIso(const std::optional& time) { + if (!time.has_value()) { + return ""; + } + return absl::FormatTime("%Y-%m-%dT%H:%M:%SZ", *time, absl::UTCTimeZone()); +} + +std::string OptionalTimeToJson(const std::optional& time) { + std::string iso = OptionalTimeToIso(time); + if (iso.empty()) { + return "null"; + } + return absl::StrCat("\"", JsonEscape(iso), "\""); +} + +std::string OptionalTimeToYaml(const std::optional& time) { + std::string iso = OptionalTimeToIso(time); + if (iso.empty()) { + return "null"; + } + return iso; +} + +const char* TestRunStatusToString(TestRunStatus status) { + switch (status) { + case TestRunStatus::kQueued: + return "QUEUED"; + case TestRunStatus::kRunning: + return "RUNNING"; + case TestRunStatus::kPassed: + return "PASSED"; + case TestRunStatus::kFailed: + return "FAILED"; + case TestRunStatus::kTimeout: + return "TIMEOUT"; + case TestRunStatus::kUnknown: + default: + return "UNKNOWN"; + } +} + +bool IsTerminalStatus(TestRunStatus status) { + switch (status) { + case TestRunStatus::kQueued: + case TestRunStatus::kRunning: + return false; + case TestRunStatus::kPassed: + case TestRunStatus::kFailed: + case TestRunStatus::kTimeout: + case TestRunStatus::kUnknown: + default: + return true; + } +} + +std::optional ParseStatusFilter(absl::string_view value) { + std::string lower = std::string(absl::AsciiStrToLower(value)); + if (lower == "queued") return TestRunStatus::kQueued; + if (lower == "running") return TestRunStatus::kRunning; + if (lower == "passed") return TestRunStatus::kPassed; + if (lower == "failed") return TestRunStatus::kFailed; + if (lower == "timeout") return TestRunStatus::kTimeout; + if (lower == "unknown") return TestRunStatus::kUnknown; + return std::nullopt; +} + +std::optional ParseWidgetTypeFilter(absl::string_view value) { + std::string lower = std::string(absl::AsciiStrToLower(value)); + if (lower.empty() || lower == "unspecified" || lower == "any") { + return WidgetTypeFilter::kUnspecified; + } + if (lower == "all") { + return WidgetTypeFilter::kAll; + } + if (lower == "button" || lower == "buttons") { + return WidgetTypeFilter::kButton; + } + if (lower == "input" || lower == "textbox" || lower == "field") { + return WidgetTypeFilter::kInput; + } + if (lower == "menu" || lower == "menuitem" || lower == "menu-item") { + return WidgetTypeFilter::kMenu; + } + if (lower == "tab" || lower == "tabs") { + return WidgetTypeFilter::kTab; + } + if (lower == "checkbox" || lower == "toggle") { + return WidgetTypeFilter::kCheckbox; + } + if (lower == "slider" || lower == "drag" || lower == "sliderfloat") { + return WidgetTypeFilter::kSlider; + } + if (lower == "canvas" || lower == "viewport") { + return WidgetTypeFilter::kCanvas; + } + if (lower == "selectable" || lower == "list-item") { + return WidgetTypeFilter::kSelectable; + } + if (lower == "other") { + return WidgetTypeFilter::kOther; + } + return std::nullopt; +} + +} // namespace agent +} // namespace cli +} // namespace yaze diff --git a/src/cli/handlers/agent/common.h b/src/cli/handlers/agent/common.h new file mode 100644 index 00000000..cf60f307 --- /dev/null +++ b/src/cli/handlers/agent/common.h @@ -0,0 +1,30 @@ +#ifndef YAZE_CLI_HANDLERS_AGENT_COMMON_H_ +#define YAZE_CLI_HANDLERS_AGENT_COMMON_H_ + +#include +#include + +#include "absl/time/time.h" +#include "cli/service/gui_automation_client.h" + +namespace yaze { +namespace cli { +namespace agent { + +std::string HarnessAddress(const std::string& host, int port); +std::string JsonEscape(absl::string_view value); +std::string YamlQuote(absl::string_view value); +std::string FormatOptionalTime(const std::optional& time); +std::string OptionalTimeToIso(const std::optional& time); +std::string OptionalTimeToJson(const std::optional& time); +std::string OptionalTimeToYaml(const std::optional& time); +const char* TestRunStatusToString(TestRunStatus status); +bool IsTerminalStatus(TestRunStatus status); +std::optional ParseStatusFilter(absl::string_view value); +std::optional ParseWidgetTypeFilter(absl::string_view value); + +} // namespace agent +} // namespace cli +} // namespace yaze + +#endif // YAZE_CLI_HANDLERS_AGENT_COMMON_H_ diff --git a/src/cli/handlers/agent/general_commands.cc b/src/cli/handlers/agent/general_commands.cc new file mode 100644 index 00000000..b28961b3 --- /dev/null +++ b/src/cli/handlers/agent/general_commands.cc @@ -0,0 +1,485 @@ +#include "cli/handlers/agent/commands.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/flags/declare.h" +#include "absl/flags/flag.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_replace.h" +#include "cli/handlers/agent/common.h" +#include "cli/modern_cli.h" +#include "cli/service/ai_service.h" +#include "cli/service/proposal_registry.h" +#include "cli/service/resource_catalog.h" +#include "cli/service/rom_sandbox_manager.h" +#include "cli/z3ed.h" +#include "util/macro.h" + +ABSL_DECLARE_FLAG(std::string, rom); + + +namespace yaze { +namespace cli { +namespace agent { + +namespace { + +struct DescribeOptions { + std::optional resource; + std::string format = "json"; + std::optional output_path; + std::string version = "0.1.0"; + std::optional last_updated; +}; + +absl::StatusOr ParseDescribeArgs( + const std::vector& args) { + DescribeOptions options; + for (size_t i = 0; i < args.size(); ++i) { + const std::string& token = args[i]; + std::string flag = token; + std::optional inline_value; + + if (absl::StartsWith(token, "--")) { + auto eq_pos = token.find('='); + if (eq_pos != std::string::npos) { + flag = token.substr(0, eq_pos); + inline_value = token.substr(eq_pos + 1); + } + } + + auto require_value = + [&](absl::string_view flag_name) -> absl::StatusOr { + if (inline_value.has_value()) { + return *inline_value; + } + if (i + 1 >= args.size()) { + return absl::InvalidArgumentError( + absl::StrFormat("Flag %s requires a value", flag_name)); + } + return args[++i]; + }; + + if (flag == "--resource") { + ASSIGN_OR_RETURN(auto value, require_value("--resource")); + options.resource = std::move(value); + } else if (flag == "--format") { + ASSIGN_OR_RETURN(auto value, require_value("--format")); + options.format = std::move(value); + } else if (flag == "--output") { + ASSIGN_OR_RETURN(auto value, require_value("--output")); + options.output_path = std::move(value); + } else if (flag == "--version") { + ASSIGN_OR_RETURN(auto value, require_value("--version")); + options.version = std::move(value); + } else if (flag == "--last-updated") { + ASSIGN_OR_RETURN(auto value, require_value("--last-updated")); + options.last_updated = std::move(value); + } else { + return absl::InvalidArgumentError( + absl::StrFormat("Unknown flag for agent describe: %s", token)); + } + } + + options.format = absl::AsciiStrToLower(options.format); + if (options.format != "json" && options.format != "yaml") { + return absl::InvalidArgumentError("--format must be either json or yaml"); + } + + return options; +} + +} // namespace + +absl::Status HandleRunCommand(const std::vector& arg_vec, + Rom& rom) { + if (arg_vec.size() < 2 || arg_vec[0] != "--prompt") { + return absl::InvalidArgumentError("Usage: agent run --prompt "); + } + std::string prompt = arg_vec[1]; + + if (!rom.is_loaded()) { + std::string rom_path = absl::GetFlag(FLAGS_rom); + if (rom_path.empty()) { + return absl::FailedPreconditionError( + "No ROM loaded. Use --rom= to specify ROM file.\n" + "Example: z3ed agent run --rom=zelda3.sfc --prompt \"Your prompt here\""); + } + + auto status = rom.LoadFromFile(rom_path); + if (!status.ok()) { + return absl::FailedPreconditionError( + absl::StrFormat("Failed to load ROM from '%s': %s", rom_path, + status.message())); + } + } + + auto sandbox_or = RomSandboxManager::Instance().CreateSandbox(rom, + "agent-run"); + if (!sandbox_or.ok()) { + return sandbox_or.status(); + } + auto sandbox = sandbox_or.value(); + + auto proposal_or = ProposalRegistry::Instance().CreateProposal( + sandbox.id, prompt, "Agent-generated ROM modifications"); + if (!proposal_or.ok()) { + return proposal_or.status(); + } + auto proposal = proposal_or.value(); + + RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( + proposal.id, absl::StrCat("Starting agent run with prompt: ", prompt))); + + MockAIService ai_service; + auto commands_or = ai_service.GetCommands(prompt); + if (!commands_or.ok()) { + RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( + proposal.id, + absl::StrCat("AI service error: ", commands_or.status().message()))); + return commands_or.status(); + } + std::vector commands = commands_or.value(); + + RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( + proposal.id, absl::StrCat("Generated ", commands.size(), + " commands"))); + + ModernCLI cli; + int commands_executed = 0; + for (const auto& command : commands) { + RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( + proposal.id, absl::StrCat("Executing: ", command))); + + std::vector command_parts; + std::string current_part; + bool in_quotes = false; + for (char c : command) { + if (c == '"') { + in_quotes = !in_quotes; + } else if (c == ' ' && !in_quotes) { + command_parts.push_back(current_part); + current_part.clear(); + } else { + current_part += c; + } + } + command_parts.push_back(current_part); + + if (command_parts.size() < 2) { + auto error_msg = absl::StrFormat("Malformed command: %s", command); + RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog(proposal.id, + error_msg)); + return absl::InvalidArgumentError(error_msg); + } + + std::string cmd_name = command_parts[0] + " " + command_parts[1]; + std::vector cmd_args(command_parts.begin() + 2, + command_parts.end()); + + auto it = cli.commands_.find(cmd_name); + if (it != cli.commands_.end()) { + auto status = it->second.handler(cmd_args); + if (!status.ok()) { + RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( + proposal.id, absl::StrCat("Command failed: ", status.message()))); + return status; + } + commands_executed++; + RETURN_IF_ERROR( + ProposalRegistry::Instance().AppendLog(proposal.id, + "Command succeeded")); + } else { + auto error_msg = absl::StrCat("Unknown command: ", cmd_name); + RETURN_IF_ERROR( + ProposalRegistry::Instance().AppendLog(proposal.id, error_msg)); + return absl::NotFoundError(error_msg); + } + } + + RETURN_IF_ERROR(ProposalRegistry::Instance().AppendLog( + proposal.id, + absl::StrCat("Completed execution of ", commands_executed, + " commands"))); + + std::cout << "βœ… Agent run completed successfully." << std::endl; + std::cout << " Proposal ID: " << proposal.id << std::endl; + std::cout << " Sandbox: " << sandbox.rom_path << std::endl; + std::cout << " Use 'z3ed agent diff' to review changes" << std::endl; + + return absl::OkStatus(); +} + +absl::Status HandlePlanCommand(const std::vector& arg_vec) { + if (arg_vec.size() < 2 || arg_vec[0] != "--prompt") { + return absl::InvalidArgumentError("Usage: agent plan --prompt "); + } + std::string prompt = arg_vec[1]; + MockAIService ai_service; + auto commands_or = ai_service.GetCommands(prompt); + if (!commands_or.ok()) { + return commands_or.status(); + } + std::vector commands = commands_or.value(); + + std::cout << "AI Agent Plan:" << std::endl; + for (const auto& command : commands) { + std::cout << " - " << command << std::endl; + } + return absl::OkStatus(); +} + +absl::Status HandleDiffCommand(Rom& rom, + const std::vector& args) { + std::optional proposal_id; + for (size_t i = 0; i < args.size(); ++i) { + const std::string& token = args[i]; + if (absl::StartsWith(token, "--proposal-id=")) { + proposal_id = token.substr(14); + } else if (token == "--proposal-id" && i + 1 < args.size()) { + proposal_id = args[i + 1]; + ++i; + } + } + + auto& registry = ProposalRegistry::Instance(); + absl::StatusOr proposal_or; + if (proposal_id.has_value()) { + proposal_or = registry.GetProposal(proposal_id.value()); + } else { + proposal_or = registry.GetLatestPendingProposal(); + } + + if (proposal_or.ok()) { + const auto& proposal = proposal_or.value(); + + std::cout << "\n=== Proposal Diff ===\n"; + std::cout << "Proposal ID: " << proposal.id << "\n"; + std::cout << "Sandbox ID: " << proposal.sandbox_id << "\n"; + std::cout << "Prompt: " << proposal.prompt << "\n"; + std::cout << "Description: " << proposal.description << "\n"; + std::cout << "Status: "; + switch (proposal.status) { + case ProposalRegistry::ProposalStatus::kPending: + std::cout << "Pending"; + break; + case ProposalRegistry::ProposalStatus::kAccepted: + std::cout << "Accepted"; + break; + case ProposalRegistry::ProposalStatus::kRejected: + std::cout << "Rejected"; + break; + } + std::cout << "\n"; + std::cout << "Created: " << absl::FormatTime(proposal.created_at) + << "\n"; + std::cout << "Commands Executed: " << proposal.commands_executed + << "\n"; + std::cout << "Bytes Changed: " << proposal.bytes_changed << "\n\n"; + + if (std::filesystem::exists(proposal.diff_path)) { + std::cout << "--- Diff Content ---\n"; + std::ifstream diff_file(proposal.diff_path); + if (diff_file.is_open()) { + std::string line; + while (std::getline(diff_file, line)) { + std::cout << line << "\n"; + } + } else { + std::cout << "(Unable to read diff file)\n"; + } + } else { + std::cout << "(No diff file found)\n"; + } + + std::cout << "\n--- Execution Log ---\n"; + if (std::filesystem::exists(proposal.log_path)) { + std::ifstream log_file(proposal.log_path); + if (log_file.is_open()) { + std::string line; + int line_count = 0; + while (std::getline(log_file, line)) { + std::cout << line << "\n"; + line_count++; + if (line_count > 50) { + std::cout << "... (log truncated, see " << proposal.log_path + << " for full output)\n"; + break; + } + } + } else { + std::cout << "(Unable to read log file)\n"; + } + } else { + std::cout << "(No log file found)\n"; + } + + std::cout << "\n=== Next Steps ===\n"; + std::cout << "To accept changes: z3ed agent commit\n"; + std::cout << "To reject changes: z3ed agent revert\n"; + std::cout << "To review in GUI: yaze --proposal=" << proposal.id << "\n"; + + return absl::OkStatus(); + } + + if (rom.is_loaded()) { + auto sandbox_or = RomSandboxManager::Instance().ActiveSandbox(); + if (!sandbox_or.ok()) { + return absl::NotFoundError( + "No pending proposals found and no active sandbox. Run 'z3ed agent run' first."); + } + RomDiff diff_handler; + auto status = + diff_handler.Run({rom.filename(), sandbox_or->rom_path.string()}); + if (!status.ok()) { + return status; + } + } else { + return absl::AbortedError("No ROM loaded."); + } + return absl::OkStatus(); +} + +absl::Status HandleLearnCommand() { + std::cout << "Agent learn not yet implemented." << std::endl; + return absl::OkStatus(); +} + +absl::Status HandleListCommand() { + auto& registry = ProposalRegistry::Instance(); + auto proposals = registry.ListProposals(); + + if (proposals.empty()) { + std::cout << "No proposals found.\n"; + std::cout + << "Run 'z3ed agent run --prompt \"...\"' to create a proposal.\n"; + return absl::OkStatus(); + } + + std::cout << "\n=== Agent Proposals ===\n\n"; + + for (const auto& proposal : proposals) { + std::cout << "ID: " << proposal.id << "\n"; + std::cout << " Status: "; + switch (proposal.status) { + case ProposalRegistry::ProposalStatus::kPending: + std::cout << "Pending"; + break; + case ProposalRegistry::ProposalStatus::kAccepted: + std::cout << "Accepted"; + break; + case ProposalRegistry::ProposalStatus::kRejected: + std::cout << "Rejected"; + break; + } + std::cout << "\n"; + std::cout << " Created: " << absl::FormatTime(proposal.created_at) + << "\n"; + std::cout << " Prompt: " << proposal.prompt << "\n"; + std::cout << " Commands: " << proposal.commands_executed << "\n"; + std::cout << " Bytes Changed: " << proposal.bytes_changed << "\n"; + std::cout << "\n"; + } + + std::cout << "Total: " << proposals.size() << " proposal(s)\n"; + std::cout << "\nUse 'z3ed agent diff --proposal-id=' to view details.\n"; + + return absl::OkStatus(); +} + +absl::Status HandleCommitCommand(Rom& rom) { + if (rom.is_loaded()) { + auto status = rom.SaveToFile({.save_new = false}); + if (!status.ok()) { + return status; + } + std::cout << "βœ… Changes committed successfully." << std::endl; + } else { + return absl::AbortedError("No ROM loaded."); + } + return absl::OkStatus(); +} + +absl::Status HandleRevertCommand(Rom& rom) { + if (rom.is_loaded()) { + auto status = rom.LoadFromFile(rom.filename()); + if (!status.ok()) { + return status; + } + std::cout << "βœ… Changes reverted successfully." << std::endl; + } else { + return absl::AbortedError("No ROM loaded."); + } + return absl::OkStatus(); +} + +absl::Status HandleDescribeCommand(const std::vector& arg_vec) { + ASSIGN_OR_RETURN(auto options, ParseDescribeArgs(arg_vec)); + + const auto& catalog = ResourceCatalog::Instance(); + std::optional resource_schema; + if (options.resource.has_value()) { + auto resource_or = catalog.GetResource(*options.resource); + if (!resource_or.ok()) { + return resource_or.status(); + } + resource_schema = resource_or.value(); + } + + std::string payload; + if (options.format == "json") { + if (resource_schema.has_value()) { + payload = catalog.SerializeResource(*resource_schema); + } else { + payload = catalog.SerializeResources(catalog.AllResources()); + } + } else { + std::string last_updated = options.last_updated.has_value() + ? *options.last_updated + : absl::FormatTime("%Y-%m-%d", absl::Now(), + absl::LocalTimeZone()); + if (resource_schema.has_value()) { + std::vector schemas{*resource_schema}; + payload = catalog.SerializeResourcesAsYaml( + schemas, options.version, last_updated); + } else { + payload = catalog.SerializeResourcesAsYaml( + catalog.AllResources(), options.version, last_updated); + } + } + + if (options.output_path.has_value()) { + std::ofstream out(*options.output_path, std::ios::binary | std::ios::trunc); + if (!out.is_open()) { + return absl::InternalError(absl::StrFormat( + "Failed to open %s for writing", *options.output_path)); + } + out << payload; + out.close(); + if (!out) { + return absl::InternalError(absl::StrFormat( + "Failed to write schema to %s", *options.output_path)); + } + std::cout << absl::StrFormat("Wrote %s schema to %s", options.format, + *options.output_path) + << std::endl; + return absl::OkStatus(); + } + + std::cout << payload << std::endl; + return absl::OkStatus(); +} + +} // namespace agent +} // namespace cli +} // namespace yaze diff --git a/src/cli/handlers/agent/gui_commands.cc b/src/cli/handlers/agent/gui_commands.cc new file mode 100644 index 00000000..cc7f788a --- /dev/null +++ b/src/cli/handlers/agent/gui_commands.cc @@ -0,0 +1,334 @@ +#include "cli/handlers/agent/commands.h" + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/time/time.h" +#include "cli/handlers/agent/common.h" +#include "cli/service/gui_automation_client.h" +#include "util/macro.h" + +namespace yaze { +namespace cli { +namespace agent { + +namespace { + +absl::Status HandleGuiDiscoverCommand(const std::vector& arg_vec) { + std::string host = "localhost"; + int port = 50052; + std::string window_filter; + std::string path_prefix; + std::optional type_filter; + std::optional type_filter_label; + bool include_invisible = false; + bool include_disabled = false; + std::string format = "table"; + int limit = -1; + + auto require_value = + [&](const std::vector& args, size_t& index, + absl::string_view flag) -> absl::StatusOr { + if (index + 1 >= args.size()) { + return absl::InvalidArgumentError( + absl::StrFormat("Flag %s requires a value", flag)); + } + return args[++index]; + }; + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + if (token == "--host") { + ASSIGN_OR_RETURN(auto value, require_value(arg_vec, i, "--host")); + host = std::move(value); + } else if (absl::StartsWith(token, "--host=")) { + host = token.substr(7); + } else if (token == "--port") { + ASSIGN_OR_RETURN(auto value, require_value(arg_vec, i, "--port")); + port = std::stoi(value); + } else if (absl::StartsWith(token, "--port=")) { + port = std::stoi(token.substr(7)); + } else if (token == "--window" || token == "--window-filter") { + ASSIGN_OR_RETURN(auto value, require_value(arg_vec, i, token.c_str())); + window_filter = std::move(value); + } else if (absl::StartsWith(token, "--window=")) { + window_filter = token.substr(9); + } else if (token == "--path-prefix") { + ASSIGN_OR_RETURN(auto value, require_value(arg_vec, i, "--path-prefix")); + path_prefix = std::move(value); + } else if (absl::StartsWith(token, "--path-prefix=")) { + path_prefix = token.substr(14); + } else if (token == "--type") { + ASSIGN_OR_RETURN(auto value, require_value(arg_vec, i, "--type")); + auto parsed = ParseWidgetTypeFilter(value); + if (!parsed.has_value()) { + return absl::InvalidArgumentError( + absl::StrFormat("Unknown widget type filter: %s", value)); + } + type_filter = parsed; + type_filter_label = absl::AsciiStrToLower(value); + } else if (absl::StartsWith(token, "--type=")) { + std::string value = token.substr(7); + auto parsed = ParseWidgetTypeFilter(value); + if (!parsed.has_value()) { + return absl::InvalidArgumentError( + absl::StrFormat("Unknown widget type filter: %s", value)); + } + type_filter = parsed; + type_filter_label = absl::AsciiStrToLower(value); + } else if (token == "--include-invisible") { + include_invisible = true; + } else if (token == "--include-disabled") { + include_disabled = true; + } else if (token == "--format") { + ASSIGN_OR_RETURN(auto value, require_value(arg_vec, i, "--format")); + format = std::move(value); + } else if (absl::StartsWith(token, "--format=")) { + format = token.substr(9); + } else if (token == "--limit") { + ASSIGN_OR_RETURN(auto value, require_value(arg_vec, i, "--limit")); + limit = std::stoi(value); + } else if (absl::StartsWith(token, "--limit=")) { + limit = std::stoi(token.substr(8)); + } else if (token == "--help" || token == "-h") { + std::cout << "Usage: agent gui discover [options]\n" + << " --host \n" + << " --port \n" + << " --window \n" + << " --type \n" + << " --path-prefix \n" + << " --include-invisible\n" + << " --include-disabled\n" + << " --format \n" + << " --limit \n"; + return absl::OkStatus(); + } else { + return absl::InvalidArgumentError( + absl::StrFormat("Unknown flag for agent gui discover: %s", token)); + } + } + + format = absl::AsciiStrToLower(format); + if (format != "table" && format != "json") { + return absl::InvalidArgumentError( + "--format must be either 'table' or 'json'"); + } + + if (limit == 0) { + return absl::InvalidArgumentError("--limit must be positive"); + } + +#ifndef YAZE_WITH_GRPC + (void)host; + (void)port; + (void)window_filter; + (void)path_prefix; + (void)type_filter; + (void)include_invisible; + (void)include_disabled; + (void)format; + (void)limit; + return absl::UnimplementedError( + "GUI automation requires YAZE_WITH_GRPC=ON at build time.\n" + "Rebuild with: cmake -B build -DYAZE_WITH_GRPC=ON"); +#else + GuiAutomationClient client(HarnessAddress(host, port)); + RETURN_IF_ERROR(client.Connect()); + + DiscoverWidgetsQuery query; + query.window_filter = window_filter; + query.path_prefix = path_prefix; + if (type_filter.has_value()) { + query.type_filter = type_filter.value(); + } + query.include_invisible = include_invisible; + query.include_disabled = include_disabled; + + ASSIGN_OR_RETURN(auto response, client.DiscoverWidgets(query)); + + int max_items = limit > 0 ? limit : std::numeric_limits::max(); + int remaining = max_items; + std::vector trimmed_windows; + trimmed_windows.reserve(response.windows.size()); + int rendered_widgets = 0; + + for (const auto& window : response.windows) { + if (remaining <= 0) { + break; + } + DiscoveredWindowInfo trimmed; + trimmed.name = window.name; + trimmed.visible = window.visible; + + for (const auto& widget : window.widgets) { + if (remaining <= 0) { + break; + } + trimmed.widgets.push_back(widget); + --remaining; + ++rendered_widgets; + } + + if (!trimmed.widgets.empty()) { + trimmed_windows.push_back(std::move(trimmed)); + } + } + + bool truncated = rendered_widgets < response.total_widgets; + + if (format == "json") { + std::cout << "{\n"; + std::cout << " \"server\": \"" << JsonEscape(HarnessAddress(host, port)) + << "\",\n"; + std::cout << " \"totalWidgets\": " << response.total_widgets << ",\n"; + std::cout << " \"returnedWidgets\": " << rendered_widgets << ",\n"; + std::cout << " \"truncated\": " << (truncated ? "true" : "false") << ",\n"; + std::cout << " \"generatedAt\": " + << (response.generated_at.has_value() + ? absl::StrCat( + "\"", + JsonEscape(absl::FormatTime("%Y-%m-%dT%H:%M:%SZ", + *response.generated_at, + absl::UTCTimeZone())), + "\"") + : std::string("null")) + << ",\n"; + std::cout << " \"windows\": [\n"; + + for (size_t w = 0; w < trimmed_windows.size(); ++w) { + const auto& window = trimmed_windows[w]; + std::cout << " {\n"; + std::cout << " \"name\": \"" << JsonEscape(window.name) << "\",\n"; + std::cout << " \"visible\": " << (window.visible ? "true" : "false") + << ",\n"; + std::cout << " \"widgets\": [\n"; + for (size_t i = 0; i < window.widgets.size(); ++i) { + const auto& widget = window.widgets[i]; + std::cout << " {\n"; + std::cout << " \"path\": \"" << JsonEscape(widget.path) + << "\",\n"; + std::cout << " \"label\": \"" << JsonEscape(widget.label) + << "\",\n"; + std::cout << " \"type\": \"" << JsonEscape(widget.type) + << "\",\n"; + std::cout << " \"description\": \"" + << JsonEscape(widget.description) << "\",\n"; + std::cout << " \"suggestedAction\": \"" + << JsonEscape(widget.suggested_action) << "\",\n"; + std::cout << " \"visible\": " + << (widget.visible ? "true" : "false") << ",\n"; + std::cout << " \"enabled\": " + << (widget.enabled ? "true" : "false") << ",\n"; + std::cout << " \"bounds\": { \"min\": [" << widget.bounds.min_x + << ", " << widget.bounds.min_y << "], \"max\": [" + << widget.bounds.max_x << ", " << widget.bounds.max_y + << "] },\n"; + std::cout << " \"widgetId\": " << widget.widget_id << "\n"; + std::cout << " }"; + if (i + 1 < window.widgets.size()) { + std::cout << ","; + } + std::cout << "\n"; + } + std::cout << " ]\n"; + std::cout << " }"; + if (w + 1 < trimmed_windows.size()) { + std::cout << ","; + } + std::cout << "\n"; + } + + std::cout << " ]\n"; + std::cout << "}\n"; + return absl::OkStatus(); + } + + std::cout << "\n=== Widget Discovery ===\n"; + std::cout << "Server: " << HarnessAddress(host, port) << "\n"; + if (!window_filter.empty()) { + std::cout << "Window filter: " << window_filter << "\n"; + } + if (!path_prefix.empty()) { + std::cout << "Path prefix: " << path_prefix << "\n"; + } + if (type_filter_label.has_value()) { + std::cout << "Type filter: " << *type_filter_label << "\n"; + } + std::cout << "Include invisible: " << (include_invisible ? "yes" : "no") + << "\n"; + std::cout << "Include disabled: " << (include_disabled ? "yes" : "no") + << "\n\n"; + + if (trimmed_windows.empty()) { + std::cout << "No widgets matched the provided filters." << std::endl; + return absl::OkStatus(); + } + + for (const auto& window : trimmed_windows) { + std::cout << "Window: " << window.name + << (window.visible ? " (visible)" : " (hidden)") << "\n"; + for (const auto& widget : window.widgets) { + std::cout << " β€’ [" << widget.type << "] " << widget.label << "\n"; + std::cout << " Path: " << widget.path << "\n"; + if (!widget.description.empty()) { + std::cout << " Description: " << widget.description << "\n"; + } + std::cout << " Suggested: " << widget.suggested_action << "\n"; + std::cout << " State: " << (widget.visible ? "visible" : "hidden") + << ", " << (widget.enabled ? "enabled" : "disabled") << "\n"; + std::cout << absl::StrFormat(" Bounds: (%.1f, %.1f) β†’ (%.1f, %.1f)\n", + widget.bounds.min_x, widget.bounds.min_y, + widget.bounds.max_x, widget.bounds.max_y); + std::cout << " Widget ID: 0x" << std::hex << widget.widget_id + << std::dec << "\n"; + } + std::cout << "\n"; + } + + std::cout << "Widgets shown: " << rendered_widgets << " of " + << response.total_widgets; + if (truncated) { + std::cout << " (truncated)"; + } + std::cout << "\n"; + + if (response.generated_at.has_value()) { + std::cout << "Snapshot: " + << absl::FormatTime("%Y-%m-%d %H:%M:%S", *response.generated_at, + absl::LocalTimeZone()) + << "\n"; + } + + return absl::OkStatus(); +#endif +} + +} // namespace + +absl::Status HandleGuiCommand(const std::vector& arg_vec) { + if (arg_vec.empty()) { + return absl::InvalidArgumentError("Usage: agent gui [options]"); + } + + const std::string& subcommand = arg_vec[0]; + std::vector tail(arg_vec.begin() + 1, arg_vec.end()); + + if (subcommand == "discover") { + return HandleGuiDiscoverCommand(tail); + } + + return absl::InvalidArgumentError( + absl::StrFormat("Unknown agent gui subcommand: %s", subcommand)); +} + +} // namespace agent +} // namespace cli +} // namespace yaze diff --git a/src/cli/handlers/agent/test_commands.cc b/src/cli/handlers/agent/test_commands.cc new file mode 100644 index 00000000..c6be042d --- /dev/null +++ b/src/cli/handlers/agent/test_commands.cc @@ -0,0 +1,839 @@ +#include "cli/handlers/agent/commands.h" + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" +#include "absl/strings/str_format.h" +#include "absl/time/time.h" +#include "cli/handlers/agent/common.h" +#include "cli/service/gui_automation_client.h" +#include "cli/service/test_workflow_generator.h" +#include "util/macro.h" + +namespace yaze { +namespace cli { +namespace agent { + +namespace { + +absl::Status HandleTestRunCommand(const std::vector& arg_vec) { + std::string prompt; + std::string host = "localhost"; + int port = 50052; + int timeout_sec = 30; + std::string output_format = "text"; + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + + if (token == "--prompt" && i + 1 < arg_vec.size()) { + prompt = arg_vec[++i]; + } else if (token == "--host" && i + 1 < arg_vec.size()) { + host = arg_vec[++i]; + } else if (token == "--port" && i + 1 < arg_vec.size()) { + port = std::stoi(arg_vec[++i]); + } else if (token == "--timeout" && i + 1 < arg_vec.size()) { + timeout_sec = std::stoi(arg_vec[++i]); + } else if (token == "--output" && i + 1 < arg_vec.size()) { + output_format = arg_vec[++i]; + } else if (absl::StartsWith(token, "--prompt=")) { + prompt = token.substr(9); + } else if (absl::StartsWith(token, "--host=")) { + host = token.substr(7); + } else if (absl::StartsWith(token, "--port=")) { + port = std::stoi(token.substr(7)); + } else if (absl::StartsWith(token, "--timeout=")) { + timeout_sec = std::stoi(token.substr(10)); + } else if (absl::StartsWith(token, "--output=")) { + output_format = token.substr(9); + } + } + + if (prompt.empty()) { + return absl::InvalidArgumentError( + "Usage: agent test --prompt \"\" [--host ] [--port " + "] [--timeout ] [--output text|json|yaml]\n\n" + "Examples:\n" + " z3ed agent test --prompt \"Open Overworld editor\"\n" + " z3ed agent test --prompt \"Open Dungeon editor and verify it " + "loads\"\n" + " z3ed agent test --prompt \"Click Open ROM button\" --output json"); + } + + output_format = absl::AsciiStrToLower(output_format); + bool text_output = (output_format == "text" || output_format == "human"); + bool json_output = (output_format == "json"); + bool yaml_output = (output_format == "yaml"); + if (!text_output && !json_output && !yaml_output) { + return absl::InvalidArgumentError( + "--output must be one of: text, json, yaml"); + } + bool machine_output = !text_output; + +#ifndef YAZE_WITH_GRPC + std::string error = + "GUI automation requires YAZE_WITH_GRPC=ON at build time.\n" + "Rebuild with: cmake -B build -DYAZE_WITH_GRPC=ON"; + if (machine_output) { + if (json_output) { + std::cout << "{\n" + << " \"prompt\": \"" << JsonEscape(prompt) << "\",\n" + << " \"success\": false,\n" + << " \"error\": \"" << JsonEscape(error) << "\"\n" + << "}\n"; + } else { + std::cout << "prompt: " << YamlQuote(prompt) << "\n" + << "success: false\n" + << "error: " << YamlQuote(error) << "\n"; + } + } else { + std::cout << error << std::endl; + } + return absl::UnimplementedError(error); +#else + struct StepSummary { + std::string description; + bool success = false; + int64_t duration_ms = 0; + std::string message; + std::string test_id; + }; + + std::vector step_summaries; + std::vector emitted_test_ids; + std::chrono::steady_clock::time_point start_time; + bool timer_started = false; + + auto EmitMachineSummary = [&](bool success, absl::string_view error_message, + int64_t elapsed_override_ms = -1) { + if (!machine_output) { + return; + } + + int64_t elapsed_ms = elapsed_override_ms; + if (elapsed_ms < 0) { + if (timer_started) { + elapsed_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time) + .count(); + } else { + elapsed_ms = 0; + } + } + + std::string primary_test_id = + emitted_test_ids.empty() ? "" : emitted_test_ids.back(); + + if (json_output) { + std::cout << "{\n"; + std::cout << " \"prompt\": \"" << JsonEscape(prompt) << "\",\n"; + std::cout << " \"host\": \"" << JsonEscape(host) << "\",\n"; + std::cout << " \"port\": " << port << ",\n"; + std::cout << " \"success\": " << (success ? "true" : "false") << ",\n"; + std::cout << " \"timeout_seconds\": " << timeout_sec << ",\n"; + if (!primary_test_id.empty()) { + std::cout << " \"test_id\": \"" << JsonEscape(primary_test_id) + << "\",\n"; + } else { + std::cout << " \"test_id\": null,\n"; + } + std::cout << " \"test_ids\": ["; + for (size_t i = 0; i < emitted_test_ids.size(); ++i) { + if (i > 0) { + std::cout << ", "; + } + std::cout << "\"" << JsonEscape(emitted_test_ids[i]) << "\""; + } + std::cout << "],\n"; + std::cout << " \"elapsed_ms\": " << elapsed_ms << ",\n"; + std::cout << " \"steps\": [\n"; + for (size_t i = 0; i < step_summaries.size(); ++i) { + const auto& step = step_summaries[i]; + std::string message_json = + step.message.empty() + ? "null" + : absl::StrCat("\"", JsonEscape(step.message), "\""); + std::string test_id_json = + step.test_id.empty() + ? "null" + : absl::StrCat("\"", JsonEscape(step.test_id), "\""); + std::cout << " {\n"; + std::cout << " \"index\": " << (i + 1) << ",\n"; + std::cout << " \"description\": \"" << JsonEscape(step.description) + << "\",\n"; + std::cout << " \"success\": " << (step.success ? "true" : "false") + << ",\n"; + std::cout << " \"duration_ms\": " << step.duration_ms << ",\n"; + std::cout << " \"message\": " << message_json << ",\n"; + std::cout << " \"test_id\": " << test_id_json << "\n"; + std::cout << " }"; + if (i + 1 < step_summaries.size()) { + std::cout << ","; + } + std::cout << "\n"; + } + std::cout << " ],\n"; + if (!error_message.empty()) { + std::cout << " \"error\": \"" << JsonEscape(std::string(error_message)) + << "\"\n"; + } else { + std::cout << " \"error\": null\n"; + } + std::cout << "}\n"; + } else if (yaml_output) { + std::cout << "prompt: " << YamlQuote(prompt) << "\n"; + std::cout << "host: " << YamlQuote(host) << "\n"; + std::cout << "port: " << port << "\n"; + std::cout << "success: " << (success ? "true" : "false") << "\n"; + std::cout << "timeout_seconds: " << timeout_sec << "\n"; + if (primary_test_id.empty()) { + std::cout << "test_id: null\n"; + } else { + std::cout << "test_id: " << YamlQuote(primary_test_id) << "\n"; + } + if (emitted_test_ids.empty()) { + std::cout << "test_ids: []\n"; + } else { + std::cout << "test_ids:\n"; + for (const auto& id : emitted_test_ids) { + std::cout << " - " << YamlQuote(id) << "\n"; + } + } + std::cout << "elapsed_ms: " << elapsed_ms << "\n"; + if (step_summaries.empty()) { + std::cout << "steps: []\n"; + } else { + std::cout << "steps:\n"; + for (size_t i = 0; i < step_summaries.size(); ++i) { + const auto& step = step_summaries[i]; + std::cout << " - index: " << (i + 1) << "\n"; + std::cout << " description: " << YamlQuote(step.description) + << "\n"; + std::cout << " success: " << (step.success ? "true" : "false") + << "\n"; + std::cout << " duration_ms: " << step.duration_ms << "\n"; + if (step.message.empty()) { + std::cout << " message: null\n"; + } else { + std::cout << " message: " << YamlQuote(step.message) << "\n"; + } + if (step.test_id.empty()) { + std::cout << " test_id: null\n"; + } else { + std::cout << " test_id: " << YamlQuote(step.test_id) << "\n"; + } + } + } + if (!error_message.empty()) { + std::cout << "error: " << YamlQuote(std::string(error_message)) << "\n"; + } else { + std::cout << "error: null\n"; + } + } + }; + + if (text_output) { + std::cout << "\n=== GUI Automation Test ===\n"; + std::cout << "Prompt: " << prompt << "\n"; + std::cout << "Server: " << host << ":" << port << "\n\n"; + } + + TestWorkflowGenerator generator; + auto workflow_or = generator.GenerateWorkflow(prompt); + if (!workflow_or.ok()) { + EmitMachineSummary(false, workflow_or.status().message()); + return workflow_or.status(); + } + auto workflow = workflow_or.value(); + + if (text_output) { + std::cout << "Generated workflow:\n" << workflow.ToString() << "\n"; + } + + GuiAutomationClient client(HarnessAddress(host, port)); + auto connect_status = client.Connect(); + if (!connect_status.ok()) { + std::string formatted_error = absl::StrFormat( + "Failed to connect to test harness at %s:%d\n" + "Make sure YAZE is running with:\n" + " ./yaze --enable_test_harness --test_harness_port=%d " + "--rom_file=\n\n" + "Error: %s", + host, port, port, connect_status.message()); + EmitMachineSummary(false, formatted_error); + return absl::UnavailableError(formatted_error); + } + + if (text_output) { + std::cout << "βœ“ Connected to test harness\n\n"; + } + + start_time = std::chrono::steady_clock::now(); + timer_started = true; + int step_num = 0; + + for (const auto& step : workflow.steps) { + step_num++; + StepSummary summary; + summary.description = step.ToString(); + + if (text_output) { + std::cout << absl::StrFormat("[%d/%d] %s ... ", step_num, + workflow.steps.size(), summary.description); + std::cout.flush(); + } + + absl::StatusOr result; + + switch (step.type) { + case TestStepType::kClick: + result = client.Click(step.target); + break; + case TestStepType::kType: + result = client.Type(step.target, step.text, step.clear_first); + break; + case TestStepType::kWait: + result = client.Wait(step.condition, step.timeout_ms); + break; + case TestStepType::kAssert: + result = client.Assert(step.condition); + break; + case TestStepType::kScreenshot: + result = client.Screenshot(); + break; + } + + if (!result.ok()) { + summary.success = false; + summary.message = result.status().message(); + step_summaries.push_back(std::move(summary)); + if (text_output) { + std::cout << "βœ— FAILED\n"; + } + EmitMachineSummary(false, result.status().message()); + return absl::InternalError(absl::StrFormat("Step %d failed: %s", step_num, + result.status().message())); + } + + summary.duration_ms = result->execution_time.count(); + summary.message = result->message; + + if (!result->success) { + summary.success = false; + if (!result->test_id.empty()) { + summary.test_id = result->test_id; + emitted_test_ids.push_back(result->test_id); + } + step_summaries.push_back(std::move(summary)); + if (text_output) { + std::cout << "βœ— FAILED\n"; + std::cout << " Error: " << result->message << "\n"; + } + EmitMachineSummary(false, result->message); + return absl::InternalError( + absl::StrFormat("Step %d failed: %s", step_num, result->message)); + } + + summary.success = true; + if (!result->test_id.empty()) { + summary.test_id = result->test_id; + emitted_test_ids.push_back(result->test_id); + } + step_summaries.push_back(summary); + + if (text_output) { + std::cout << absl::StrFormat("βœ“ (%lldms)", + result->execution_time.count()); + if (!result->test_id.empty()) { + std::cout << " [Test ID: " << result->test_id << "]"; + } + std::cout << "\n"; + } + } + + auto end_time = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast( + end_time - start_time); + + if (text_output) { + std::cout << "\nβœ… Test passed in " << elapsed.count() << "ms\n"; + + if (!emitted_test_ids.empty()) { + std::cout << "Latest Test ID: " << emitted_test_ids.back() << "\n"; + if (emitted_test_ids.size() > 1) { + std::cout << "Captured Test IDs:\n"; + for (const auto& id : emitted_test_ids) { + std::cout << " - " << id << "\n"; + } + } + std::cout << "Use 'z3ed agent test status --test-id " + << emitted_test_ids.back() << "' for live status updates." + << std::endl; + } + } + + EmitMachineSummary(true, /*error_message=*/"", elapsed.count()); + return absl::OkStatus(); +#endif +} + +absl::Status HandleTestStatusCommand(const std::vector& arg_vec) { + std::string host = "localhost"; + int port = 50052; + std::string test_id; + bool follow = false; + int interval_ms = 1000; + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + + if (token == "--test-id" && i + 1 < arg_vec.size()) { + test_id = arg_vec[++i]; + } else if (absl::StartsWith(token, "--test-id=")) { + test_id = token.substr(10); + } else if (token == "--host" && i + 1 < arg_vec.size()) { + host = arg_vec[++i]; + } else if (absl::StartsWith(token, "--host=")) { + host = token.substr(7); + } else if (token == "--port" && i + 1 < arg_vec.size()) { + port = std::stoi(arg_vec[++i]); + } else if (absl::StartsWith(token, "--port=")) { + port = std::stoi(token.substr(7)); + } else if (token == "--follow") { + follow = true; + } else if ((token == "--interval" || token == "--interval-ms") && + i + 1 < arg_vec.size()) { + interval_ms = std::max(100, std::stoi(arg_vec[++i])); + } else if (absl::StartsWith(token, "--interval=") || + absl::StartsWith(token, "--interval-ms=")) { + size_t prefix = token.find('='); + interval_ms = std::max(100, std::stoi(token.substr(prefix + 1))); + } + } + + if (test_id.empty()) { + return absl::InvalidArgumentError( + "Usage: agent test status --test-id [--follow] [--host ] " + "[--port ] [--interval-ms ]"); + } + +#ifndef YAZE_WITH_GRPC + return absl::UnimplementedError( + "GUI automation requires YAZE_WITH_GRPC=ON at build time.\n" + "Rebuild with: cmake -B build -DYAZE_WITH_GRPC=ON"); +#else + GuiAutomationClient client(HarnessAddress(host, port)); + RETURN_IF_ERROR(client.Connect()); + + std::cout << "\n=== Test Status ===\n"; + std::cout << "Test ID: " << test_id << "\n"; + std::cout << "Server: " << HarnessAddress(host, port) << "\n"; + if (follow) { + std::cout << "Follow mode: polling every " << interval_ms << "ms\n"; + } + std::cout << "\n"; + + bool first_iteration = true; + while (true) { + ASSIGN_OR_RETURN(auto details, client.GetTestStatus(test_id)); + + if (!first_iteration) { + std::cout << "---\n"; + } + + std::cout << "Status: " << TestRunStatusToString(details.status) << "\n"; + std::cout << "Queued At: " << FormatOptionalTime(details.queued_at) << "\n"; + std::cout << "Started At: " << FormatOptionalTime(details.started_at) + << "\n"; + std::cout << "Completed At: " << FormatOptionalTime(details.completed_at) + << "\n"; + std::cout << "Execution Time (ms): " << details.execution_time_ms << "\n"; + if (!details.error_message.empty()) { + std::cout << "Error: " << details.error_message << "\n"; + } + + if (!details.assertion_failures.empty()) { + std::cout << "Assertion Failures (" << details.assertion_failures.size() + << "):\n"; + for (const auto& failure : details.assertion_failures) { + std::cout << " - " << failure << "\n"; + } + } else { + std::cout << "Assertion Failures: 0\n"; + } + + if (!follow || IsTerminalStatus(details.status)) { + break; + } + + first_iteration = false; + std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms)); + } + + return absl::OkStatus(); +#endif +} + +absl::Status HandleTestListCommand(const std::vector& arg_vec) { + std::string host = "localhost"; + int port = 50052; + std::string category_filter; + std::optional status_filter; + int page_size = 100; + int limit = -1; + bool fetch_all = false; + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + + if (token == "--host" && i + 1 < arg_vec.size()) { + host = arg_vec[++i]; + } else if (absl::StartsWith(token, "--host=")) { + host = token.substr(7); + } else if (token == "--port" && i + 1 < arg_vec.size()) { + port = std::stoi(arg_vec[++i]); + } else if (absl::StartsWith(token, "--port=")) { + port = std::stoi(token.substr(7)); + } else if (token == "--category" && i + 1 < arg_vec.size()) { + category_filter = arg_vec[++i]; + } else if (absl::StartsWith(token, "--category=")) { + category_filter = token.substr(11); + } else if (token == "--status" && i + 1 < arg_vec.size()) { + auto parsed = ParseStatusFilter(arg_vec[++i]); + if (!parsed.has_value()) { + return absl::InvalidArgumentError( + "Invalid status filter. Expected: queued, running, passed, failed, " + "timeout, unknown"); + } + status_filter = parsed; + } else if (absl::StartsWith(token, "--status=")) { + auto parsed = ParseStatusFilter(token.substr(9)); + if (!parsed.has_value()) { + return absl::InvalidArgumentError( + "Invalid status filter. Expected: queued, running, passed, failed, " + "timeout, unknown"); + } + status_filter = parsed; + } else if (token == "--page-size" && i + 1 < arg_vec.size()) { + page_size = std::max(1, std::stoi(arg_vec[++i])); + } else if (absl::StartsWith(token, "--page-size=")) { + page_size = std::max(1, std::stoi(token.substr(12))); + } else if (token == "--limit" && i + 1 < arg_vec.size()) { + limit = std::stoi(arg_vec[++i]); + } else if (absl::StartsWith(token, "--limit=")) { + limit = std::stoi(token.substr(8)); + } else if (token == "--all") { + fetch_all = true; + } + } + + if (fetch_all) { + limit = -1; + } + +#ifndef YAZE_WITH_GRPC + return absl::UnimplementedError( + "GUI automation requires YAZE_WITH_GRPC=ON at build time.\n" + "Rebuild with: cmake -B build -DYAZE_WITH_GRPC=ON"); +#else + GuiAutomationClient client(HarnessAddress(host, port)); + RETURN_IF_ERROR(client.Connect()); + + std::cout << "\n=== Harness Test Catalog ===\n"; + std::cout << "Server: " << HarnessAddress(host, port) << "\n"; + if (!category_filter.empty()) { + std::cout << "Category filter: " << category_filter << "\n"; + } + if (status_filter.has_value()) { + std::cout << "Status filter: " + << TestRunStatusToString(status_filter.value()) << "\n"; + } + std::cout << "\n"; + + std::vector collected; + collected.reserve(limit > 0 ? limit : page_size); + std::string page_token; + int total_count = 0; + + while (true) { + int request_page_size = page_size > 0 ? page_size : 100; + if (limit > 0) { + int remaining = limit - static_cast(collected.size()); + if (remaining <= 0) { + break; + } + request_page_size = std::min(request_page_size, remaining); + } + + ASSIGN_OR_RETURN( + auto batch, + client.ListTests(category_filter, request_page_size, page_token)); + + total_count = batch.total_count; + + for (const auto& summary : batch.tests) { + if (status_filter.has_value()) { + ASSIGN_OR_RETURN(auto details, client.GetTestStatus(summary.test_id)); + if (details.status != status_filter.value()) { + continue; + } + } + + collected.push_back(summary); + if (limit > 0 && static_cast(collected.size()) >= limit) { + break; + } + } + + if (limit > 0 && static_cast(collected.size()) >= limit) { + break; + } + + if (batch.next_page_token.empty()) { + break; + } + page_token = batch.next_page_token; + } + + if (collected.empty()) { + std::cout << "No tests found for the specified filters." << std::endl; + return absl::OkStatus(); + } + + for (const auto& summary : collected) { + std::cout << "Test ID: " << summary.test_id << "\n"; + std::cout << " Name: " << summary.name << "\n"; + std::cout << " Category: " << summary.category << "\n"; + std::cout << " Last Run: " << FormatOptionalTime(summary.last_run_at) + << "\n"; + std::cout << " Runs: " << summary.total_runs << " (" << summary.pass_count + << " pass / " << summary.fail_count << " fail)\n"; + std::cout << " Average Duration (ms): " << summary.average_duration_ms + << "\n\n"; + } + + std::cout << "Displayed " << collected.size() << " test(s)"; + if (total_count > 0) { + std::cout << " (catalog size: " << total_count << ")"; + } + std::cout << "." << std::endl; + + return absl::OkStatus(); +#endif +} + +absl::Status HandleTestResultsCommand(const std::vector& arg_vec) { + std::string host = "localhost"; + int port = 50052; + std::string test_id; + bool include_logs = false; + std::string format = "yaml"; + + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& token = arg_vec[i]; + + if (token == "--test-id" && i + 1 < arg_vec.size()) { + test_id = arg_vec[++i]; + } else if (absl::StartsWith(token, "--test-id=")) { + test_id = token.substr(10); + } else if (token == "--host" && i + 1 < arg_vec.size()) { + host = arg_vec[++i]; + } else if (absl::StartsWith(token, "--host=")) { + host = token.substr(7); + } else if (token == "--port" && i + 1 < arg_vec.size()) { + port = std::stoi(arg_vec[++i]); + } else if (absl::StartsWith(token, "--port=")) { + port = std::stoi(token.substr(7)); + } else if (token == "--include-logs") { + include_logs = true; + } else if (token == "--format" && i + 1 < arg_vec.size()) { + format = absl::AsciiStrToLower(arg_vec[++i]); + } else if (absl::StartsWith(token, "--format=")) { + format = absl::AsciiStrToLower(token.substr(9)); + } + } + + if (test_id.empty()) { + return absl::InvalidArgumentError( + "Usage: agent test results --test-id [--include-logs] [--format " + "yaml|json] [--host ] [--port ]"); + } + + if (format != "yaml" && format != "json") { + return absl::InvalidArgumentError( + "--format must be either 'yaml' or 'json'"); + } + +#ifndef YAZE_WITH_GRPC + return absl::UnimplementedError( + "GUI automation requires YAZE_WITH_GRPC=ON at build time.\n" + "Rebuild with: cmake -B build -DYAZE_WITH_GRPC=ON"); +#else + GuiAutomationClient client(HarnessAddress(host, port)); + RETURN_IF_ERROR(client.Connect()); + + ASSIGN_OR_RETURN(auto details, client.GetTestResults(test_id, include_logs)); + + if (format == "json") { + std::cout << "{\n"; + std::cout << " \"test_id\": \"" << JsonEscape(details.test_id) << "\",\n"; + std::cout << " \"success\": " << (details.success ? "true" : "false") + << ",\n"; + std::cout << " \"name\": \"" << JsonEscape(details.test_name) << "\",\n"; + std::cout << " \"category\": \"" << JsonEscape(details.category) + << "\",\n"; + std::cout << " \"executed_at\": \"" + << JsonEscape(FormatOptionalTime(details.executed_at)) << "\",\n"; + std::cout << " \"duration_ms\": " << details.duration_ms << ",\n"; + + std::cout << " \"assertions\": "; + if (details.assertions.empty()) { + std::cout << "[],\n"; + } else { + std::cout << "[\n"; + for (size_t i = 0; i < details.assertions.size(); ++i) { + const auto& assertion = details.assertions[i]; + std::cout << " {\"description\": \"" + << JsonEscape(assertion.description) << "\", \"passed\": " + << (assertion.passed ? "true" : "false"); + if (!assertion.expected_value.empty()) { + std::cout << ", \"expected\": \"" + << JsonEscape(assertion.expected_value) << "\""; + } + if (!assertion.actual_value.empty()) { + std::cout << ", \"actual\": \"" << JsonEscape(assertion.actual_value) + << "\""; + } + if (!assertion.error_message.empty()) { + std::cout << ", \"error\": \"" << JsonEscape(assertion.error_message) + << "\""; + } + std::cout << "}"; + if (i + 1 < details.assertions.size()) { + std::cout << ","; + } + std::cout << "\n"; + } + std::cout << " ],\n"; + } + + std::cout << " \"logs\": "; + if (include_logs && !details.logs.empty()) { + std::cout << "[\n"; + for (size_t i = 0; i < details.logs.size(); ++i) { + std::cout << " \"" << JsonEscape(details.logs[i]) << "\""; + if (i + 1 < details.logs.size()) { + std::cout << ","; + } + std::cout << "\n"; + } + std::cout << " ],\n"; + } else { + std::cout << "[],\n"; + } + + std::cout << " \"metrics\": "; + if (!details.metrics.empty()) { + std::cout << "{\n"; + size_t index = 0; + for (const auto& [key, value] : details.metrics) { + std::cout << " \"" << JsonEscape(key) << "\": " << value; + if (index + 1 < details.metrics.size()) { + std::cout << ","; + } + std::cout << "\n"; + ++index; + } + std::cout << " }\n"; + } else { + std::cout << "{}\n"; + } + + std::cout << "}" << std::endl; + } else { + std::cout << "test_id: " << details.test_id << "\n"; + std::cout << "success: " << (details.success ? "true" : "false") << "\n"; + std::cout << "name: " << YamlQuote(details.test_name) << "\n"; + std::cout << "category: " << YamlQuote(details.category) << "\n"; + std::cout << "executed_at: " << FormatOptionalTime(details.executed_at) + << "\n"; + std::cout << "duration_ms: " << details.duration_ms << "\n"; + + if (details.assertions.empty()) { + std::cout << "assertions: []\n"; + } else { + std::cout << "assertions:\n"; + for (const auto& assertion : details.assertions) { + std::cout << " - description: " << YamlQuote(assertion.description) + << "\n"; + std::cout << " passed: " << (assertion.passed ? "true" : "false") + << "\n"; + if (!assertion.expected_value.empty()) { + std::cout << " expected: " << YamlQuote(assertion.expected_value) + << "\n"; + } + if (!assertion.actual_value.empty()) { + std::cout << " actual: " << YamlQuote(assertion.actual_value) + << "\n"; + } + if (!assertion.error_message.empty()) { + std::cout << " error: " << YamlQuote(assertion.error_message) + << "\n"; + } + } + } + + if (include_logs && !details.logs.empty()) { + std::cout << "logs:\n"; + for (const auto& log : details.logs) { + std::cout << " - " << YamlQuote(log) << "\n"; + } + } else { + std::cout << "logs: []\n"; + } + + if (details.metrics.empty()) { + std::cout << "metrics: {}\n"; + } else { + std::cout << "metrics:\n"; + for (const auto& [key, value] : details.metrics) { + std::cout << " " << key << ": " << value << "\n"; + } + } + } + + return absl::OkStatus(); +#endif +} + +} // namespace + +absl::Status HandleTestCommand(const std::vector& arg_vec) { + if (!arg_vec.empty()) { + const std::string& subcommand = arg_vec[0]; + std::vector tail(arg_vec.begin() + 1, arg_vec.end()); + + if (subcommand == "status") { + return HandleTestStatusCommand(tail); + } + if (subcommand == "list") { + return HandleTestListCommand(tail); + } + if (subcommand == "results") { + return HandleTestResultsCommand(tail); + } + } + + return HandleTestRunCommand(arg_vec); +} + +} // namespace agent +} // namespace cli +} // namespace yaze diff --git a/src/cli/handlers/patch.cc b/src/cli/handlers/patch.cc index cc35babf..113a7e8a 100644 --- a/src/cli/handlers/patch.cc +++ b/src/cli/handlers/patch.cc @@ -52,7 +52,7 @@ absl::Status AsarPatch::Run(const std::vector& arg_vec) { return absl::AbortedError("Failed to load ROM."); } - app::core::AsarWrapper wrapper; + core::AsarWrapper wrapper; auto init_status = wrapper.Initialize(); if (!init_status.ok()) { return init_status; diff --git a/src/cli/modern_cli.cc b/src/cli/modern_cli.cc index 67e16372..4c8795f0 100644 --- a/src/cli/modern_cli.cc +++ b/src/cli/modern_cli.cc @@ -330,7 +330,7 @@ absl::Status ModernCLI::HandleRomInfoCommand(const std::vector& arg absl::Status ModernCLI::HandleExtractSymbolsCommand(const std::vector& args) { // Use the AsarWrapper to extract symbols - yaze::app::core::AsarWrapper wrapper; + yaze::core::AsarWrapper wrapper; RETURN_IF_ERROR(wrapper.Initialize()); auto symbols_result = wrapper.ExtractSymbols(args[0]); diff --git a/src/cli/tui.cc b/src/cli/tui.cc index 712a9695..fb06a4a5 100644 --- a/src/cli/tui.cc +++ b/src/cli/tui.cc @@ -326,7 +326,7 @@ void ExtractSymbolsComponent(ftxui::ScreenInteractive &screen) { } try { - app::core::AsarWrapper wrapper; + core::AsarWrapper wrapper; auto init_status = wrapper.Initialize(); if (!init_status.ok()) { app_context.error_message = absl::StrCat("Failed to initialize Asar: ", init_status.message()); @@ -419,7 +419,7 @@ void ValidateAssemblyComponent(ftxui::ScreenInteractive &screen) { } try { - app::core::AsarWrapper wrapper; + core::AsarWrapper wrapper; auto init_status = wrapper.Initialize(); if (!init_status.ok()) { app_context.error_message = absl::StrCat("Failed to initialize Asar: ", init_status.message()); diff --git a/src/cli/tui/asar_patch.cc b/src/cli/tui/asar_patch.cc index 79b15f15..f6bc6d6c 100644 --- a/src/cli/tui/asar_patch.cc +++ b/src/cli/tui/asar_patch.cc @@ -35,7 +35,7 @@ ftxui::Component AsarPatchComponent::Render() { } try { - app::core::AsarWrapper wrapper; + core::AsarWrapper wrapper; auto init_status = wrapper.Initialize(); if (!init_status.ok()) { app_context.error_message = absl::StrCat("Failed to initialize Asar: ", init_status.message()); diff --git a/src/cli/z3ed.cmake b/src/cli/z3ed.cmake index 51caf381..01358831 100644 --- a/src/cli/z3ed.cmake +++ b/src/cli/z3ed.cmake @@ -42,6 +42,10 @@ add_executable( cli/handlers/command_palette.cc cli/handlers/project.cc cli/handlers/agent.cc + cli/handlers/agent/common.cc + cli/handlers/agent/general_commands.cc + cli/handlers/agent/test_commands.cc + cli/handlers/agent/gui_commands.cc cli/service/ai_service.cc cli/service/proposal_registry.cc cli/service/resource_catalog.cc diff --git a/test/integration/asar_integration_test.cc b/test/integration/asar_integration_test.cc index a80fe05e..862c78e5 100644 --- a/test/integration/asar_integration_test.cc +++ b/test/integration/asar_integration_test.cc @@ -17,7 +17,7 @@ namespace integration { class AsarIntegrationTest : public ::testing::Test { protected: void SetUp() override { - wrapper_ = std::make_unique(); + wrapper_ = std::make_unique(); // Create test directory test_dir_ = std::filesystem::temp_directory_path() / "yaze_asar_integration"; @@ -322,7 +322,7 @@ error_test: err_file.close(); } - std::unique_ptr wrapper_; + std::unique_ptr wrapper_; std::filesystem::path test_dir_; std::filesystem::path comprehensive_asm_path_; std::filesystem::path advanced_asm_path_; diff --git a/test/integration/asar_rom_test.cc b/test/integration/asar_rom_test.cc index be3217fc..1389c892 100644 --- a/test/integration/asar_rom_test.cc +++ b/test/integration/asar_rom_test.cc @@ -19,14 +19,14 @@ class AsarRomIntegrationTest : public RomDependentTest { protected: void SetUp() override { RomDependentTest::SetUp(); - - wrapper_ = std::make_unique(); + + wrapper_ = std::make_unique(); ASSERT_OK(wrapper_->Initialize()); - + // Create test directory test_dir_ = std::filesystem::temp_directory_path() / "yaze_asar_rom_test"; std::filesystem::create_directories(test_dir_); - + CreateTestPatches(); } @@ -226,7 +226,7 @@ enemy_shell: symbols_file.close(); } - std::unique_ptr wrapper_; + std::unique_ptr wrapper_; std::filesystem::path test_dir_; std::filesystem::path simple_patch_path_; std::filesystem::path gameplay_patch_path_; @@ -239,15 +239,16 @@ TEST_F(AsarRomIntegrationTest, SimplePatchOnRealRom) { size_t original_size = rom_copy.size(); // Apply simple patch - auto patch_result = wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy); + auto patch_result = + wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy); ASSERT_OK(patch_result.status()); const auto& result = patch_result.value(); - EXPECT_TRUE(result.success) << "Patch failed: " - << testing::PrintToString(result.errors); + EXPECT_TRUE(result.success) + << "Patch failed: " << testing::PrintToString(result.errors); // Verify ROM was modified - EXPECT_NE(rom_copy, test_rom_); // Should be different + EXPECT_NE(rom_copy, test_rom_); // Should be different EXPECT_GE(rom_copy.size(), original_size); // Size may have grown // Check for expected symbols @@ -277,17 +278,16 @@ TEST_F(AsarRomIntegrationTest, SymbolExtractionFromRealRom) { // Check for specific symbols we expect std::vector expected_symbols = { - "main_routine", "init_player", "game_loop", "update_player", - "update_enemies", "update_graphics", "multiply_by_two", "divide_by_two" - }; + "main_routine", "init_player", "game_loop", "update_player", + "update_enemies", "update_graphics", "multiply_by_two", "divide_by_two"}; for (const auto& expected_symbol : expected_symbols) { bool found = false; for (const auto& symbol : symbols) { if (symbol.name == expected_symbol) { found = true; - EXPECT_GT(symbol.address, 0) << "Symbol " << expected_symbol - << " has invalid address"; + EXPECT_GT(symbol.address, 0) + << "Symbol " << expected_symbol << " has invalid address"; break; } } @@ -311,19 +311,20 @@ TEST_F(AsarRomIntegrationTest, GameplayModificationPatch) { std::vector rom_copy = test_rom_; // Apply gameplay modification patch - auto patch_result = wrapper_->ApplyPatch(gameplay_patch_path_.string(), rom_copy); + auto patch_result = + wrapper_->ApplyPatch(gameplay_patch_path_.string(), rom_copy); ASSERT_OK(patch_result.status()); const auto& result = patch_result.value(); - EXPECT_TRUE(result.success) << "Gameplay patch failed: " - << testing::PrintToString(result.errors); + EXPECT_TRUE(result.success) + << "Gameplay patch failed: " << testing::PrintToString(result.errors); // Verify specific memory locations were modified // Note: These addresses are based on the patch content - + // Check health modification at 0x7EF36C -> ROM offset would need calculation // For a proper test, we'd need to convert SNES addresses to ROM offsets - + // Check if custom routine was inserted at 0xC000 -> ROM offset 0x18000 (in LoROM) const uint32_t rom_offset = 0x18000; // Bank $00:C000 in LoROM if (rom_offset < rom_copy.size()) { @@ -369,40 +370,43 @@ broken_routine: broken_file.close(); std::vector rom_copy = test_rom_; - auto patch_result = wrapper_->ApplyPatch(broken_patch_path.string(), rom_copy); + auto patch_result = + wrapper_->ApplyPatch(broken_patch_path.string(), rom_copy); // Should fail with proper error messages EXPECT_FALSE(patch_result.ok()); - EXPECT_THAT(patch_result.status().message(), - testing::AnyOf( - testing::HasSubstr("invalid"), - testing::HasSubstr("unknown"), - testing::HasSubstr("error"))); + EXPECT_THAT(patch_result.status().message(), + testing::AnyOf(testing::HasSubstr("invalid"), + testing::HasSubstr("unknown"), + testing::HasSubstr("error"))); } TEST_F(AsarRomIntegrationTest, PatchValidationWorkflow) { // Test the complete workflow: validate -> patch -> verify - + // Step 1: Validate assembly - auto validation_result = wrapper_->ValidateAssembly(simple_patch_path_.string()); + auto validation_result = + wrapper_->ValidateAssembly(simple_patch_path_.string()); EXPECT_OK(validation_result); // Step 2: Apply patch std::vector rom_copy = test_rom_; - auto patch_result = wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy); + auto patch_result = + wrapper_->ApplyPatch(simple_patch_path_.string(), rom_copy); ASSERT_OK(patch_result.status()); EXPECT_TRUE(patch_result->success); // Step 3: Verify results EXPECT_GT(patch_result->symbols.size(), 0); EXPECT_GT(patch_result->rom_size, 0); - + // Step 4: Test symbol operations auto entry_symbol = wrapper_->FindSymbol("yaze_test_entry"); EXPECT_TRUE(entry_symbol.has_value()); - + if (entry_symbol) { - auto symbols_at_address = wrapper_->GetSymbolsAtAddress(entry_symbol->address); + auto symbols_at_address = + wrapper_->GetSymbolsAtAddress(entry_symbol->address); EXPECT_GT(symbols_at_address.size(), 0); } } diff --git a/test/unit/core/asar_wrapper_test.cc b/test/unit/core/asar_wrapper_test.cc index 36619d81..07c9fb07 100644 --- a/test/unit/core/asar_wrapper_test.cc +++ b/test/unit/core/asar_wrapper_test.cc @@ -7,7 +7,6 @@ #include namespace yaze { -namespace app { namespace core { namespace { @@ -321,5 +320,4 @@ TEST_F(AsarWrapperTest, CreatePatchNotImplemented) { } // namespace } // namespace core -} // namespace app } // namespace yaze