Files
tai/taicpp/tests/mplex_flow_test.cpp
T

168 lines
6.3 KiB
C++
Raw Normal View History

// Differential test: validate_flow_spec and PlexerFlow.run against tai.py.
// usage: mplex_flow_test <corpus.jsonl produced by mplex_flow_gen.py>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include "plexer_flow.h"
using detz::mplex::Json;
using namespace detz::plexer;
namespace {
class Script : public PlexerController {
public:
explicit Script(std::vector<Json> replies_in) : replies(std::move(replies_in)) { kind = "script"; }
std::vector<Json> replies;
size_t next = 0;
Json send_keys(const std::string&, bool) override { return Json(); }
Json read_pane(int) override { return Json(); }
Json status() override { return Json(); }
Json ask(const Json&, double) override {
if (next >= replies.size()) {
Json out = Json::object();
out.set("status", Json::string("cancelled"));
return out;
}
return replies[next++];
}
};
} // namespace
// Real tmux 3.6 round trip (tmux is the oracle): prints one line per observation so the same
// steps run through tai.py's TmuxPlexerController can be diffed (see mplex_tmux_oracle.py).
int tmux_mode() {
char dir[] = "/tmp/mplex-tmux-XXXXXX";
if (!mkdtemp(dir)) return 2;
setenv("TMUX_TMPDIR", dir, 1);
unsetenv("TMUX");
if (std::system("tmux new-session -d -s t -x 80 -y 24 'cat' >/dev/null 2>&1") != 0) return 2;
FILE* pipe = popen("tmux list-panes -t t -F '#{pane_id}'", "r");
char buf[64] = {0};
if (!pipe || !std::fgets(buf, sizeof buf, pipe)) return 2;
pclose(pipe);
std::string pane(buf);
while (!pane.empty() && (pane.back() == '\n' || pane.back() == '\r')) pane.pop_back();
TmuxPlexerController c(pane);
std::printf("send %s\n", c.send_keys("hello world", true).dumps().c_str());
usleep(300000);
std::string content = c.read_pane(5).as_string();
std::printf("read %s\n", Json::string(content).dumps().c_str());
std::printf("status %s\n", c.status().dumps().c_str());
try {
TmuxPlexerController bad("%99");
bad.read_pane(3);
} catch (const PlexerError& e) {
std::printf("error %s\n", e.what());
}
Json screen = Json::object();
screen.set("kind", Json::string("weird"));
try {
c.ask(screen, 5);
} catch (const PlexerError& e) {
std::printf("ask %s\n", e.what());
}
screen.set("kind", Json::string("choice"));
screen.set("title", Json::string("T"));
screen.set("options", Json::loads("[\"a\", \"b\"]"));
try {
std::printf("choice %s\n", c.ask(screen, 5).dumps().c_str());
} catch (const PlexerError& e) {
std::printf("choice error %s\n", e.what());
}
screen.set("kind", Json::string("text"));
screen.set("text", Json::string("question"));
try {
std::printf("text %s\n", c.ask(screen, 5).dumps().c_str());
} catch (const PlexerError& e) {
std::printf("text error %s\n", e.what());
}
if (std::system("tmux kill-server >/dev/null 2>&1") != 0) return 3;
return 0;
}
// Drives the native controller against a running mplex server (env TAI_PLEXER_SOCKET/PANE):
// status keys, send, read, then a whole PlexerFlow app answered by the user at the terminal.
int native_mode() {
auto controller = PlexerControllerFactory::detect();
if (!controller) {
std::puts("no controller");
return 2;
}
Json status = controller->status();
std::string keys;
for (const auto& item : status.as_object()) keys += item.first + ",";
std::printf("status-keys %s kind %s\n", keys.c_str(), controller->kind.c_str());
std::printf("send %s\n", controller->send_keys("echo native-$((20+22))", true).dumps().c_str());
usleep(600000);
std::string tail = controller->read_pane(30).as_string();
std::printf("read-has-marker %d\n", tail.find("native-42") != std::string::npos);
Json spec = Json::loads(
"{\"title\": \"Deploy\", \"timeout\": 30, \"screens\": [{\"id\": \"main\", \"title\": \"Where\", \"body\": \"Pick.\","
" \"fields\": [{\"id\": \"env\", \"type\": \"choice\", \"label\": \"Environment\", \"options\": [\"staging\", \"prod\"]},"
" {\"id\": \"tag\", \"type\": \"text\", \"label\": \"Tag\", \"initial\": \"v1\", \"show_if\": {\"env\": \"prod\"}}],"
" \"actions\": [{\"id\": \"go\", \"label\": \"Deploy\"}, {\"id\": \"back\", \"label\": \"Back\"}]}]}");
auto [app, err] = validate_flow_spec(spec);
if (!err.empty()) {
std::printf("spec error %s\n", err.c_str());
return 2;
}
std::fflush(stdout);
PlexerFlow flow(app);
std::printf("outcome %s\n", flow.run(*controller).dumps().c_str());
return 0;
}
int main(int argc, char** argv) {
if (argc >= 2 && std::strcmp(argv[1], "--tmux") == 0) return tmux_mode();
if (argc >= 2 && std::strcmp(argv[1], "--native") == 0) return native_mode();
if (argc < 2) {
std::fputs("usage: mplex_flow_test corpus.jsonl\n", stderr);
return 2;
}
std::ifstream in(argv[1]);
std::string line;
int specs = 0, flows = 0, accepted = 0, failures = 0;
while (std::getline(in, line)) {
if (line.empty()) continue;
Json item = Json::loads(line);
const std::string kind = item.get("kind")->as_string();
if (kind == "spec") {
++specs;
auto [app, err] = validate_flow_spec(*item.get("spec"));
const Json& want_app = *item.get("app");
const std::string& want_err = item.get("err")->as_string();
std::string got_app = app.is_null() ? "null" : app.dumps();
if (!app.is_null()) ++accepted;
if (got_app != want_app.dumps() || err != want_err) {
++failures;
if (failures < 15)
std::cerr << "SPEC MISMATCH\n spec: " << item.get("spec")->dumps() << "\n want: " << want_err << " | "
<< want_app.dumps() << "\n got: " << err << " | " << got_app << "\n";
}
} else if (kind == "flow") {
++flows;
std::vector<Json> replies = item.get("replies")->as_array();
Script script(replies);
PlexerFlow flow(*item.get("app"));
Json outcome = flow.run(script);
if (outcome.dumps() != item.get("outcome")->dumps()) {
++failures;
if (failures < 15)
std::cerr << "FLOW MISMATCH\n app: " << item.get("app")->dumps() << "\n want: " << item.get("outcome")->dumps()
<< "\n got: " << outcome.dumps() << "\n";
}
}
}
std::printf("spec cases %d (accepted %d), flow cases %d, failures %d\n", specs, accepted, flows, failures);
return failures ? 1 : 0;
}