// Differential test: validate_flow_spec and PlexerFlow.run against tai.py. // usage: mplex_flow_test #include #include #include #include #include #include #include #include #include "mplex_plexer_flow.h" using detz::mplex::Json; using namespace detz::mplex_flow; namespace { class Script : public PlexerController { public: explicit Script(std::vector replies_in) : replies(std::move(replies_in)) { kind = "script"; } std::vector 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 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; }