yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
test_commands.cc
Go to the documentation of this file.
1#include "cli/handlers/commands.h"
2
3#include <filesystem>
4#include <fstream>
5#include <iostream>
6#include <optional>
7#include <string>
8#include <vector>
9
10#include "absl/status/status.h"
11#include "absl/status/statusor.h"
12#include "absl/strings/numbers.h"
13#include "absl/strings/str_cat.h"
14#include "absl/strings/str_format.h"
15#include "absl/time/time.h"
17#include "nlohmann/json.hpp"
18#include "util/macro.h"
19
20#ifdef YAZE_WITH_GRPC
23#endif
24
25namespace yaze {
26namespace cli {
27namespace agent {
28
29#ifdef YAZE_WITH_GRPC
30
31namespace {
32
33struct RecordingState {
34 std::string recording_id;
35 std::string host = "localhost";
36 int port = 50052;
37 std::string output_path;
38};
39
40std::filesystem::path RecordingStateFilePath() {
41 std::error_code ec;
42 std::filesystem::path base =
43 std::filesystem::temp_directory_path(ec);
44 if (ec) {
45 base = std::filesystem::current_path();
46 }
47 return base / "yaze" / "agent" / "recording_state.json";
48}
49
50absl::Status SaveRecordingState(const RecordingState& state) {
51 auto path = RecordingStateFilePath();
52 std::error_code ec;
53 std::filesystem::create_directories(path.parent_path(), ec);
54 nlohmann::json json;
55 json["recording_id"] = state.recording_id;
56 json["host"] = state.host;
57 json["port"] = state.port;
58 json["output_path"] = state.output_path;
59
60 std::ofstream out(path, std::ios::out | std::ios::trunc);
61 if (!out.is_open()) {
62 return absl::InternalError(absl::StrCat("Failed to write recording state to ",
63 path.string()));
64 }
65 out << json.dump(2);
66 if (!out.good()) {
67 return absl::InternalError(
68 absl::StrCat("Failed to flush recording state to ", path.string()));
69 }
70 return absl::OkStatus();
71}
72
73absl::StatusOr<RecordingState> LoadRecordingState() {
74 auto path = RecordingStateFilePath();
75 std::ifstream in(path);
76 if (!in.is_open()) {
77 return absl::NotFoundError("No active recording session found. Run 'z3ed agent test record start' first.");
78 }
79
80 nlohmann::json json;
81 try {
82 in >> json;
83 } catch (const nlohmann::json::parse_error& error) {
84 return absl::InternalError(
85 absl::StrCat("Failed to parse recording state at ", path.string(),
86 ": ", error.what()));
87 }
88
89 RecordingState state;
90 state.recording_id = json.value("recording_id", "");
91 state.host = json.value("host", "localhost");
92 state.port = json.value("port", 50052);
93 state.output_path = json.value("output_path", "");
94
95 if (state.recording_id.empty()) {
96 return absl::InvalidArgumentError(
97 absl::StrCat("Recording state at ", path.string(),
98 " is missing a recording_id"));
99 }
100
101 return state;
102}
103
104absl::Status ClearRecordingState() {
105 auto path = RecordingStateFilePath();
106 std::error_code ec;
107 std::filesystem::remove(path, ec);
108 if (ec && ec != std::errc::no_such_file_or_directory) {
109 return absl::InternalError(absl::StrCat("Failed to clear recording state: ",
110 ec.message()));
111 }
112 return absl::OkStatus();
113}
114
115std::string DefaultRecordingOutputPath() {
116 absl::Time now = absl::Now();
117 return absl::StrCat("tests/gui/recording-",
118 absl::FormatTime("%Y%m%dT%H%M%S", now,
119 absl::LocalTimeZone()),
120 ".json");
121}
122
123} // namespace
124
125// Forward declarations for subcommand handlers
126absl::Status HandleTestRunCommand(const std::vector<std::string>& args);
127absl::Status HandleTestReplayCommand(const std::vector<std::string>& args);
128absl::Status HandleTestStatusCommand(const std::vector<std::string>& args);
129absl::Status HandleTestListCommand(const std::vector<std::string>& args);
130absl::Status HandleTestResultsCommand(const std::vector<std::string>& args);
131absl::Status HandleTestRecordCommand(const std::vector<std::string>& args);
132
133absl::Status HandleTestRunCommand(const std::vector<std::string>& args) {
134 if (args.empty() || args[0] != "--prompt") {
135 return absl::InvalidArgumentError(
136 "Usage: agent test run --prompt <description> [--host <host>] [--port <port>]\n"
137 "Example: agent test run --prompt \"Open the overworld editor and verify it loads\"");
138 }
139
140 std::string prompt = args.size() > 1 ? args[1] : "";
141 std::string host = "localhost";
142 int port = 50052;
143
144 // Parse additional arguments
145 for (size_t i = 2; i < args.size(); ++i) {
146 if (args[i] == "--host" && i + 1 < args.size()) {
147 host = args[++i];
148 } else if (args[i] == "--port" && i + 1 < args.size()) {
149 port = std::stoi(args[++i]);
150 }
151 }
152
153 std::cout << "\n=== GUI Automation Test ===\n";
154 std::cout << "Prompt: " << prompt << "\n";
155 std::cout << "Server: " << host << ":" << port << "\n\n";
156
157 // Generate workflow from natural language
158 TestWorkflowGenerator generator;
159 auto workflow_or = generator.GenerateWorkflow(prompt);
160 if (!workflow_or.ok()) {
161 std::cerr << "Failed to generate workflow: " << workflow_or.status().message() << std::endl;
162 return workflow_or.status();
163 }
164
165 auto workflow = workflow_or.value();
166 std::cout << "Generated " << workflow.steps.size() << " test steps\n\n";
167
168 // Connect and execute
169 GuiAutomationClient client(absl::StrCat(host, ":", port));
170 auto status = client.Connect();
171 if (!status.ok()) {
172 std::cerr << "Failed to connect to test harness: " << status.message() << std::endl;
173 return status;
174 }
175
176 // Execute each step
177 for (size_t i = 0; i < workflow.steps.size(); ++i) {
178 const auto& step = workflow.steps[i];
179 std::cout << "Step " << (i + 1) << ": " << step.ToString() << "... ";
180
181 // Execute based on step type
182 absl::StatusOr<AutomationResult> result(absl::InternalError("Unknown step type"));
183
184 switch (step.type) {
186 result = client.Click(step.target);
187 break;
189 result = client.Type(step.target, step.text, step.clear_first);
190 break;
192 result = client.Wait(step.condition, step.timeout_ms);
193 break;
195 result = client.Assert(step.condition);
196 break;
197 default:
198 std::cout << "✗ SKIPPED (unknown type)\n";
199 continue;
200 }
201
202 if (!result.ok()) {
203 std::cout << "✗ FAILED\n";
204 std::cerr << " Error: " << result.status().message() << "\n";
205 return result.status();
206 }
207
208 if (!result.value().success) {
209 std::cout << "✗ FAILED\n";
210 std::cerr << " Error: " << result.value().message << "\n";
211 return absl::InternalError(result.value().message);
212 }
213
214 std::cout << "✓\n";
215 }
216
217 std::cout << "\n✅ Test passed!\n";
218 return absl::OkStatus();
219}
220
221absl::Status HandleTestReplayCommand(const std::vector<std::string>& args) {
222 if (args.empty()) {
223 return absl::InvalidArgumentError(
224 "Usage: agent test replay <script.json> [--host <host>] [--port <port>]\n"
225 "Example: agent test replay tests/overworld_load.json");
226 }
227
228 std::string script_path = args[0];
229 std::string host = "localhost";
230 int port = 50052;
231
232 for (size_t i = 1; i < args.size(); ++i) {
233 if (args[i] == "--host" && i + 1 < args.size()) {
234 host = args[++i];
235 } else if (args[i] == "--port" && i + 1 < args.size()) {
236 port = std::stoi(args[++i]);
237 }
238 }
239
240 std::cout << "\n=== Replay Test ===\n";
241 std::cout << "Script: " << script_path << "\n";
242 std::cout << "Server: " << host << ":" << port << "\n\n";
243
244 GuiAutomationClient client(absl::StrCat(host, ":", port));
245 auto status = client.Connect();
246 if (!status.ok()) {
247 std::cerr << "Failed to connect: " << status.message() << std::endl;
248 return status;
249 }
250
251 auto result = client.ReplayTest(script_path, false, {});
252 if (!result.ok()) {
253 std::cerr << "Replay failed: " << result.status().message() << std::endl;
254 return result.status();
255 }
256
257 if (result.value().success) {
258 std::cout << "✅ Replay succeeded\n";
259 std::cout << "Steps executed: " << result.value().steps_executed << "\n";
260 } else {
261 std::cout << "❌ Replay failed: " << result.value().message << "\n";
262 return absl::InternalError(result.value().message);
263 }
264
265 return absl::OkStatus();
266}
267
268absl::Status HandleTestStatusCommand(const std::vector<std::string>& args) {
269 std::string test_id;
270 std::string host = "localhost";
271 int port = 50052;
272
273 for (size_t i = 0; i < args.size(); ++i) {
274 if (args[i] == "--test-id" && i + 1 < args.size()) {
275 test_id = args[++i];
276 } else if (args[i] == "--host" && i + 1 < args.size()) {
277 host = args[++i];
278 } else if (args[i] == "--port" && i + 1 < args.size()) {
279 port = std::stoi(args[++i]);
280 }
281 }
282
283 if (test_id.empty()) {
284 return absl::InvalidArgumentError(
285 "Usage: agent test status --test-id <id> [--host <host>] [--port <port>]");
286 }
287
288 GuiAutomationClient client(absl::StrCat(host, ":", port));
289 auto status = client.Connect();
290 if (!status.ok()) {
291 return status;
292 }
293
294 auto details = client.GetTestStatus(test_id);
295 if (!details.ok()) {
296 return details.status();
297 }
298
299 std::cout << "\n=== Test Status ===\n";
300 std::cout << "Test ID: " << test_id << "\n";
301 std::cout << "Status: " << TestRunStatusToString(details.value().status) << "\n";
302 std::cout << "Started: " << FormatOptionalTime(details.value().started_at) << "\n";
303 std::cout << "Completed: " << FormatOptionalTime(details.value().completed_at) << "\n";
304
305 if (!details.value().error_message.empty()) {
306 std::cout << "Error: " << details.value().error_message << "\n";
307 }
308
309 return absl::OkStatus();
310}
311
312absl::Status HandleTestListCommand(const std::vector<std::string>& args) {
313 std::string host = "localhost";
314 int port = 50052;
315
316 for (size_t i = 0; i < args.size(); ++i) {
317 if (args[i] == "--host" && i + 1 < args.size()) {
318 host = args[++i];
319 } else if (args[i] == "--port" && i + 1 < args.size()) {
320 port = std::stoi(args[++i]);
321 }
322 }
323
324 GuiAutomationClient client(absl::StrCat(host, ":", port));
325 auto status = client.Connect();
326 if (!status.ok()) {
327 return status;
328 }
329
330 auto batch = client.ListTests("", 100, "");
331 if (!batch.ok()) {
332 return batch.status();
333 }
334
335 std::cout << "\n=== Available Tests ===\n";
336 std::cout << "Total: " << batch.value().total_count << "\n\n";
337
338 for (const auto& test : batch.value().tests) {
339 std::cout << "• " << test.name << "\n";
340 std::cout << " ID: " << test.test_id << "\n";
341 std::cout << " Category: " << test.category << "\n";
342 std::cout << " Runs: " << test.total_runs << " (" << test.pass_count
343 << " passed, " << test.fail_count << " failed)\n\n";
344 }
345
346 return absl::OkStatus();
347}
348
349absl::Status HandleTestResultsCommand(const std::vector<std::string>& args) {
350 std::string test_id;
351 std::string host = "localhost";
352 int port = 50052;
353 bool include_logs = false;
354
355 for (size_t i = 0; i < args.size(); ++i) {
356 if (args[i] == "--test-id" && i + 1 < args.size()) {
357 test_id = args[++i];
358 } else if (args[i] == "--host" && i + 1 < args.size()) {
359 host = args[++i];
360 } else if (args[i] == "--port" && i + 1 < args.size()) {
361 port = std::stoi(args[++i]);
362 } else if (args[i] == "--include-logs") {
363 include_logs = true;
364 }
365 }
366
367 if (test_id.empty()) {
368 return absl::InvalidArgumentError(
369 "Usage: agent test results --test-id <id> [--include-logs] [--host <host>] [--port <port>]");
370 }
371
372 GuiAutomationClient client(absl::StrCat(host, ":", port));
373 auto status = client.Connect();
374 if (!status.ok()) {
375 return status;
376 }
377
378 auto details = client.GetTestResults(test_id, include_logs);
379 if (!details.ok()) {
380 return details.status();
381 }
382
383 std::cout << "\n=== Test Results ===\n";
384 std::cout << "Test ID: " << details.value().test_id << "\n";
385 std::cout << "Name: " << details.value().test_name << "\n";
386 std::cout << "Success: " << (details.value().success ? "✓" : "✗") << "\n";
387 std::cout << "Duration: " << details.value().duration_ms << "ms\n\n";
388
389 if (!details.value().assertions.empty()) {
390 std::cout << "Assertions:\n";
391 for (const auto& assertion : details.value().assertions) {
392 std::cout << " " << (assertion.passed ? "✓" : "✗") << " "
393 << assertion.description << "\n";
394 if (!assertion.error_message.empty()) {
395 std::cout << " Error: " << assertion.error_message << "\n";
396 }
397 }
398 }
399
400 if (include_logs && !details.value().logs.empty()) {
401 std::cout << "\nLogs:\n";
402 for (const auto& log : details.value().logs) {
403 std::cout << " " << log << "\n";
404 }
405 }
406
407 return absl::OkStatus();
408}
409
410absl::Status HandleTestRecordCommand(const std::vector<std::string>& args) {
411 if (args.empty()) {
412 return absl::InvalidArgumentError(
413 "Usage: agent test record <start|stop> [options]\n"
414 " start [--output <file>] [--description <text>] [--session <id>]\n"
415 " [--host <host>] [--port <port>]\n"
416 " stop [--validate] [--discard] [--host <host>] [--port <port>]");
417 }
418
419 std::string action = args[0];
420 if (action != "start" && action != "stop") {
421 return absl::InvalidArgumentError("Record action must be 'start' or 'stop'");
422 }
423
424 if (action == "start") {
425 std::string host = "localhost";
426 int port = 50052;
427 std::string description;
428 std::string session_name;
429 std::string output_path;
430
431 for (size_t i = 1; i < args.size(); ++i) {
432 const std::string& token = args[i];
433 if (token == "--output" && i + 1 < args.size()) {
434 output_path = args[++i];
435 } else if (token == "--description" && i + 1 < args.size()) {
436 description = args[++i];
437 } else if (token == "--session" && i + 1 < args.size()) {
438 session_name = args[++i];
439 } else if (token == "--host" && i + 1 < args.size()) {
440 host = args[++i];
441 } else if (token == "--port" && i + 1 < args.size()) {
442 std::string port_value = args[++i];
443 int parsed_port = 0;
444 if (!absl::SimpleAtoi(port_value, &parsed_port)) {
445 return absl::InvalidArgumentError(
446 absl::StrCat("Invalid --port value: ", port_value));
447 }
448 port = parsed_port;
449 }
450 }
451
452 if (output_path.empty()) {
453 output_path = DefaultRecordingOutputPath();
454 }
455
456 std::filesystem::path absolute_output =
457 std::filesystem::absolute(output_path);
458 std::error_code ec;
459 std::filesystem::create_directories(absolute_output.parent_path(), ec);
460
461 GuiAutomationClient client(absl::StrCat(host, ":", port));
462 RETURN_IF_ERROR(client.Connect());
463
464 if (session_name.empty()) {
465 session_name = std::filesystem::path(output_path).stem().string();
466 }
467
468 ASSIGN_OR_RETURN(auto start_result,
469 client.StartRecording(absolute_output.string(),
470 session_name, description));
471 if (!start_result.success) {
472 return absl::InternalError(
473 absl::StrCat("Harness rejected start-recording request: ",
474 start_result.message));
475 }
476
477 RecordingState state;
478 state.recording_id = start_result.recording_id;
479 state.host = host;
480 state.port = port;
481 state.output_path = absolute_output.string();
482 RETURN_IF_ERROR(SaveRecordingState(state));
483
484 std::cout << "\n=== Recording Session Started ===\n";
485 std::cout << "Recording ID: " << start_result.recording_id << "\n";
486 std::cout << "Server: " << host << ":" << port << "\n";
487 std::cout << "Output: " << absolute_output << "\n";
488 if (!description.empty()) {
489 std::cout << "Description: " << description << "\n";
490 }
491 if (start_result.started_at.has_value()) {
492 std::cout << "Started: "
493 << absl::FormatTime("%Y-%m-%d %H:%M:%S",
494 *start_result.started_at,
495 absl::LocalTimeZone())
496 << "\n";
497 }
498 std::cout << "\nPress Ctrl+C to abort the recording session.\n";
499
500 return absl::OkStatus();
501 }
502
503 // Stop
504 bool validate = false;
505 bool discard = false;
506 std::optional<std::string> host_override;
507 std::optional<int> port_override;
508
509 for (size_t i = 1; i < args.size(); ++i) {
510 const std::string& token = args[i];
511 if (token == "--validate") {
512 validate = true;
513 } else if (token == "--discard") {
514 discard = true;
515 } else if (token == "--host" && i + 1 < args.size()) {
516 host_override = args[++i];
517 } else if (token == "--port" && i + 1 < args.size()) {
518 std::string port_value = args[++i];
519 int parsed_port = 0;
520 if (!absl::SimpleAtoi(port_value, &parsed_port)) {
521 return absl::InvalidArgumentError(
522 absl::StrCat("Invalid --port value: ", port_value));
523 }
524 port_override = parsed_port;
525 }
526 }
527
528 if (discard && validate) {
529 return absl::InvalidArgumentError(
530 "Cannot use --validate and --discard together");
531 }
532
533 ASSIGN_OR_RETURN(auto state, LoadRecordingState());
534 if (host_override.has_value()) {
535 state.host = *host_override;
536 }
537 if (port_override.has_value()) {
538 state.port = *port_override;
539 }
540
541 GuiAutomationClient client(absl::StrCat(state.host, ":", state.port));
542 RETURN_IF_ERROR(client.Connect());
543
544 ASSIGN_OR_RETURN(auto stop_result,
545 client.StopRecording(state.recording_id, discard));
546 if (!stop_result.success) {
547 return absl::InternalError(
548 absl::StrCat("Stop recording failed: ", stop_result.message));
549 }
550 RETURN_IF_ERROR(ClearRecordingState());
551
552 std::cout << "\n=== Recording Session Completed ===\n";
553 std::cout << "Recording ID: " << state.recording_id << "\n";
554 std::cout << "Server: " << state.host << ":" << state.port << "\n";
555 std::cout << "Steps captured: " << stop_result.step_count << "\n";
556 std::cout << "Duration: " << stop_result.duration.count() << " ms\n";
557 if (!stop_result.message.empty()) {
558 std::cout << "Message: " << stop_result.message << "\n";
559 }
560 if (!discard && !stop_result.output_path.empty()) {
561 std::cout << "Output saved to: " << stop_result.output_path << "\n";
562 }
563
564 if (discard) {
565 std::cout << "Recording discarded; no script file was produced." << std::endl;
566 return absl::OkStatus();
567 }
568
569 if (!validate || stop_result.output_path.empty()) {
570 std::cout << std::endl;
571 return absl::OkStatus();
572 }
573
574 std::cout << "\nReplaying recorded script to validate...\n";
575 ASSIGN_OR_RETURN(auto replay_result,
576 client.ReplayTest(stop_result.output_path, false, {}));
577 if (!replay_result.success) {
578 return absl::InternalError(
579 absl::StrCat("Replay failed: ", replay_result.message));
580 }
581
582 std::cout << "Replay succeeded. Steps executed: "
583 << replay_result.steps_executed << "\n";
584 return absl::OkStatus();
585}
586
587#endif // YAZE_WITH_GRPC
588
589absl::Status HandleTestCommand(const std::vector<std::string>& args) {
590 if (args.empty()) {
591 return absl::InvalidArgumentError(
592 "Usage: agent test <subcommand>\n"
593 "Subcommands:\n"
594 " run --prompt <text> - Generate and run a GUI automation test\n"
595 " replay <script> - Replay a recorded test script\n"
596 " status --test-id <id> - Query test execution status\n"
597 " list - List available tests\n"
598 " results --test-id <id> - Get detailed test results\n"
599 " record start/stop - Record test interactions\n"
600 "\nNote: Test commands require YAZE_WITH_GRPC=ON at build time.");
601 }
602
603#ifndef YAZE_WITH_GRPC
604 return absl::UnimplementedError(
605 "GUI automation test commands require YAZE_WITH_GRPC=ON at build time.\n"
606 "Rebuild with: cmake -B build-grpc-test -DYAZE_WITH_GRPC=ON\n"
607 "Then: cmake --build build-grpc-test --target z3ed");
608#else
609 std::string subcommand = args[0];
610 std::vector<std::string> tail(args.begin() + 1, args.end());
611
612 if (subcommand == "run") {
613 return HandleTestRunCommand(tail);
614 } else if (subcommand == "replay") {
615 return HandleTestReplayCommand(tail);
616 } else if (subcommand == "status") {
617 return HandleTestStatusCommand(tail);
618 } else if (subcommand == "list") {
619 return HandleTestListCommand(tail);
620 } else if (subcommand == "results") {
621 return HandleTestResultsCommand(tail);
622 } else if (subcommand == "record") {
623 return HandleTestRecordCommand(tail);
624 } else {
625 return absl::InvalidArgumentError(
626 absl::StrCat("Unknown test subcommand: ", subcommand,
627 "\nRun 'z3ed agent test' for usage."));
628 }
629#endif
630}
631
632} // namespace agent
633} // namespace cli
634} // namespace yaze
#define RETURN_IF_ERROR(expression)
Definition macro.h:53
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:61
std::string FormatOptionalTime(const std::optional< absl::Time > &time)
Definition common.cc:59
absl::Status HandleTestCommand(const std::vector< std::string > &args)
const char * TestRunStatusToString(TestRunStatus status)
Definition common.cc:89
Main namespace for the application.