Implement GUI Automation Test Commands and Refactor AsarWrapper Usage
- Added new test commands for GUI automation in `test_commands.cc`, including handling test runs, statuses, listings, and results. - Refactored instances of `app::core::AsarWrapper` to `core::AsarWrapper` across multiple files for consistency. - Updated CMake configuration to include new test command files. - Modified integration and unit tests to reflect the changes in AsarWrapper usage. - Ensured proper error handling and output formatting for test commands.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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/<test_id>/`; 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.
|
||||
|
||||
@@ -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/<test_id>/` 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 | <EFBFBD> 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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Server> in member variable
|
||||
#include <grpcpp/server.h>
|
||||
@@ -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_
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "app/core/widget_discovery_service.h"
|
||||
#include "app/core/service/widget_discovery_service.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
@@ -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 <string>
|
||||
#include <vector>
|
||||
@@ -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_
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "app/core/test_recorder.h"
|
||||
#include "app/core/testing/test_recorder.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
@@ -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 {
|
||||
@@ -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 <map>
|
||||
#include <string>
|
||||
@@ -52,7 +52,7 @@ class TestRecorder {
|
||||
std::vector<std::string> 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<std::string, int32_t> 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_
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "app/core/test_script_parser.h"
|
||||
#include "app/core/testing/test_script_parser.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -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 <map>
|
||||
#include <string>
|
||||
@@ -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_
|
||||
@@ -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<app::core::AsarWrapper>();
|
||||
auto asar_wrapper = std::make_unique<core::AsarWrapper>();
|
||||
RETURN_IF_ERROR(asar_wrapper->Initialize());
|
||||
|
||||
// Create backup of ROM data
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
32
src/cli/handlers/agent/commands.h
Normal file
32
src/cli/handlers/agent/commands.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#ifndef YAZE_CLI_HANDLERS_AGENT_COMMANDS_H_
|
||||
#define YAZE_CLI_HANDLERS_AGENT_COMMANDS_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
|
||||
namespace yaze {
|
||||
class Rom;
|
||||
|
||||
namespace cli {
|
||||
namespace agent {
|
||||
|
||||
absl::Status HandleRunCommand(const std::vector<std::string>& args,
|
||||
Rom& rom);
|
||||
absl::Status HandlePlanCommand(const std::vector<std::string>& args);
|
||||
absl::Status HandleDiffCommand(Rom& rom,
|
||||
const std::vector<std::string>& args);
|
||||
absl::Status HandleTestCommand(const std::vector<std::string>& args);
|
||||
absl::Status HandleGuiCommand(const std::vector<std::string>& args);
|
||||
absl::Status HandleLearnCommand();
|
||||
absl::Status HandleListCommand();
|
||||
absl::Status HandleCommitCommand(Rom& rom);
|
||||
absl::Status HandleRevertCommand(Rom& rom);
|
||||
absl::Status HandleDescribeCommand(const std::vector<std::string>& args);
|
||||
|
||||
} // namespace agent
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_CLI_HANDLERS_AGENT_COMMANDS_H_
|
||||
172
src/cli/handlers/agent/common.cc
Normal file
172
src/cli/handlers/agent/common.cc
Normal file
@@ -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<int>(c)));
|
||||
} else {
|
||||
out.push_back(static_cast<char>(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<absl::Time>& 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<absl::Time>& time) {
|
||||
if (!time.has_value()) {
|
||||
return "";
|
||||
}
|
||||
return absl::FormatTime("%Y-%m-%dT%H:%M:%SZ", *time, absl::UTCTimeZone());
|
||||
}
|
||||
|
||||
std::string OptionalTimeToJson(const std::optional<absl::Time>& time) {
|
||||
std::string iso = OptionalTimeToIso(time);
|
||||
if (iso.empty()) {
|
||||
return "null";
|
||||
}
|
||||
return absl::StrCat("\"", JsonEscape(iso), "\"");
|
||||
}
|
||||
|
||||
std::string OptionalTimeToYaml(const std::optional<absl::Time>& 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<TestRunStatus> 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<WidgetTypeFilter> 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
|
||||
30
src/cli/handlers/agent/common.h
Normal file
30
src/cli/handlers/agent/common.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef YAZE_CLI_HANDLERS_AGENT_COMMON_H_
|
||||
#define YAZE_CLI_HANDLERS_AGENT_COMMON_H_
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#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<absl::Time>& time);
|
||||
std::string OptionalTimeToIso(const std::optional<absl::Time>& time);
|
||||
std::string OptionalTimeToJson(const std::optional<absl::Time>& time);
|
||||
std::string OptionalTimeToYaml(const std::optional<absl::Time>& time);
|
||||
const char* TestRunStatusToString(TestRunStatus status);
|
||||
bool IsTerminalStatus(TestRunStatus status);
|
||||
std::optional<TestRunStatus> ParseStatusFilter(absl::string_view value);
|
||||
std::optional<WidgetTypeFilter> ParseWidgetTypeFilter(absl::string_view value);
|
||||
|
||||
} // namespace agent
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_CLI_HANDLERS_AGENT_COMMON_H_
|
||||
485
src/cli/handlers/agent/general_commands.cc
Normal file
485
src/cli/handlers/agent/general_commands.cc
Normal file
@@ -0,0 +1,485 @@
|
||||
#include "cli/handlers/agent/commands.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string> resource;
|
||||
std::string format = "json";
|
||||
std::optional<std::string> output_path;
|
||||
std::string version = "0.1.0";
|
||||
std::optional<std::string> last_updated;
|
||||
};
|
||||
|
||||
absl::StatusOr<DescribeOptions> ParseDescribeArgs(
|
||||
const std::vector<std::string>& args) {
|
||||
DescribeOptions options;
|
||||
for (size_t i = 0; i < args.size(); ++i) {
|
||||
const std::string& token = args[i];
|
||||
std::string flag = token;
|
||||
std::optional<std::string> 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<std::string> {
|
||||
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<std::string>& arg_vec,
|
||||
Rom& rom) {
|
||||
if (arg_vec.size() < 2 || arg_vec[0] != "--prompt") {
|
||||
return absl::InvalidArgumentError("Usage: agent run --prompt <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=<path> 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<std::string> 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<std::string> 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<std::string> 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<std::string>& arg_vec) {
|
||||
if (arg_vec.size() < 2 || arg_vec[0] != "--prompt") {
|
||||
return absl::InvalidArgumentError("Usage: agent plan --prompt <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<std::string> 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<std::string>& args) {
|
||||
std::optional<std::string> 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<ProposalRegistry::ProposalMetadata> 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=<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<std::string>& arg_vec) {
|
||||
ASSIGN_OR_RETURN(auto options, ParseDescribeArgs(arg_vec));
|
||||
|
||||
const auto& catalog = ResourceCatalog::Instance();
|
||||
std::optional<ResourceSchema> 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<ResourceSchema> 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
|
||||
334
src/cli/handlers/agent/gui_commands.cc
Normal file
334
src/cli/handlers/agent/gui_commands.cc
Normal file
@@ -0,0 +1,334 @@
|
||||
#include "cli/handlers/agent/commands.h"
|
||||
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string>& arg_vec) {
|
||||
std::string host = "localhost";
|
||||
int port = 50052;
|
||||
std::string window_filter;
|
||||
std::string path_prefix;
|
||||
std::optional<WidgetTypeFilter> type_filter;
|
||||
std::optional<std::string> type_filter_label;
|
||||
bool include_invisible = false;
|
||||
bool include_disabled = false;
|
||||
std::string format = "table";
|
||||
int limit = -1;
|
||||
|
||||
auto require_value =
|
||||
[&](const std::vector<std::string>& args, size_t& index,
|
||||
absl::string_view flag) -> absl::StatusOr<std::string> {
|
||||
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 <host>\n"
|
||||
<< " --port <port>\n"
|
||||
<< " --window <name>\n"
|
||||
<< " --type <widget-type>\n"
|
||||
<< " --path-prefix <path>\n"
|
||||
<< " --include-invisible\n"
|
||||
<< " --include-disabled\n"
|
||||
<< " --format <table|json>\n"
|
||||
<< " --limit <n>\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<int>::max();
|
||||
int remaining = max_items;
|
||||
std::vector<DiscoveredWindowInfo> 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<std::string>& arg_vec) {
|
||||
if (arg_vec.empty()) {
|
||||
return absl::InvalidArgumentError("Usage: agent gui <discover> [options]");
|
||||
}
|
||||
|
||||
const std::string& subcommand = arg_vec[0];
|
||||
std::vector<std::string> 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
|
||||
839
src/cli/handlers/agent/test_commands.cc
Normal file
839
src/cli/handlers/agent/test_commands.cc
Normal file
@@ -0,0 +1,839 @@
|
||||
#include "cli/handlers/agent/commands.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string>& 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 \"<prompt>\" [--host <host>] [--port "
|
||||
"<port>] [--timeout <sec>] [--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<StepSummary> step_summaries;
|
||||
std::vector<std::string> 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::milliseconds>(
|
||||
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=<rom>\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<AutomationResult> 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<std::chrono::milliseconds>(
|
||||
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<std::string>& 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 <id> [--follow] [--host <host>] "
|
||||
"[--port <port>] [--interval-ms <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<std::string>& arg_vec) {
|
||||
std::string host = "localhost";
|
||||
int port = 50052;
|
||||
std::string category_filter;
|
||||
std::optional<TestRunStatus> 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<HarnessTestSummary> 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<int>(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<int>(collected.size()) >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (limit > 0 && static_cast<int>(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<std::string>& 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 <id> [--include-logs] [--format "
|
||||
"yaml|json] [--host <host>] [--port <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<std::string>& arg_vec) {
|
||||
if (!arg_vec.empty()) {
|
||||
const std::string& subcommand = arg_vec[0];
|
||||
std::vector<std::string> 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
|
||||
@@ -52,7 +52,7 @@ absl::Status AsarPatch::Run(const std::vector<std::string>& 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;
|
||||
|
||||
@@ -330,7 +330,7 @@ absl::Status ModernCLI::HandleRomInfoCommand(const std::vector<std::string>& arg
|
||||
|
||||
absl::Status ModernCLI::HandleExtractSymbolsCommand(const std::vector<std::string>& 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]);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace integration {
|
||||
class AsarIntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
wrapper_ = std::make_unique<app::core::AsarWrapper>();
|
||||
wrapper_ = std::make_unique<core::AsarWrapper>();
|
||||
|
||||
// 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<app::core::AsarWrapper> wrapper_;
|
||||
std::unique_ptr<core::AsarWrapper> wrapper_;
|
||||
std::filesystem::path test_dir_;
|
||||
std::filesystem::path comprehensive_asm_path_;
|
||||
std::filesystem::path advanced_asm_path_;
|
||||
|
||||
@@ -19,14 +19,14 @@ class AsarRomIntegrationTest : public RomDependentTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
RomDependentTest::SetUp();
|
||||
|
||||
wrapper_ = std::make_unique<app::core::AsarWrapper>();
|
||||
|
||||
wrapper_ = std::make_unique<core::AsarWrapper>();
|
||||
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<app::core::AsarWrapper> wrapper_;
|
||||
std::unique_ptr<core::AsarWrapper> 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<std::string> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <filesystem>
|
||||
|
||||
namespace yaze {
|
||||
namespace app {
|
||||
namespace core {
|
||||
namespace {
|
||||
|
||||
@@ -321,5 +320,4 @@ TEST_F(AsarWrapperTest, CreatePatchNotImplemented) {
|
||||
|
||||
} // namespace
|
||||
} // namespace core
|
||||
} // namespace app
|
||||
} // namespace yaze
|
||||
|
||||
Reference in New Issue
Block a user