Everything compiles; main.cpp still uses the removed placeholder agent API. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
168 lines
6.3 KiB
C++
168 lines
6.3 KiB
C++
// 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 "mplex_plexer_flow.h"
|
|
|
|
using detz::mplex::Json;
|
|
using namespace detz::mplex_flow;
|
|
|
|
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;
|
|
}
|