Files
tai/taicpp/tests/shell_parity_driver.cpp
T

267 lines
14 KiB
C++
Raw Normal View History

// shell_parity_driver: runs one shell/util function of the C++ port per invocation and prints a
// canonical text form. tests/shell_parity.py runs the same function of ../tai.py, prints the same
// form, and diffs the two byte for byte.
#include "ansi.h"
#include "audio.h"
#include "diffrender.h"
#include "edge.h"
#include "glow.h"
#include "jsonvalue.h"
#include "lanes.h"
#include "livestream.h"
#include "markdown.h"
#include "pyhighlight.h"
#include "runlive.h"
#include "sandbox.h"
#include "session.h"
#include "spinner.h"
#include "targets.h"
#include "text.h"
#include "toolargs.h"
#include "utf8.h"
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <sstream>
#include <unistd.h>
using namespace detz;
using namespace detz::shell;
namespace {
const char kRs = '\x1e', kUs = '\x1f';
std::string J(const std::string& s) { return JsonValue::String(s).Dump(); }
std::string Join(const std::vector<std::string>& v, const std::string& sep) { return Utf8::Join(v, sep); }
void Out(const std::string& s) { std::fwrite(s.data(), 1, s.size(), stdout); }
long L(const std::string& s) { return std::strtol(s.c_str(), nullptr, 10); }
double D(const std::string& s) { return std::strtod(s.c_str(), nullptr); }
std::optional<std::string> Opt(const std::string& s) { if (s == "\x01NONE") return std::nullopt; return s; }
std::string FromHex(const std::string& h) {
std::string o;
for (size_t i = 0; i + 1 < h.size(); i += 2) o.push_back((char)std::strtol(h.substr(i, 2).c_str(), nullptr, 16));
return o;
}
std::string ToHex(const std::string& raw) {
static const char* d = "0123456789abcdef";
std::string o;
for (unsigned char c : raw) { o.push_back(d[c >> 4]); o.push_back(d[c & 15]); }
return o;
}
struct MemorySocket : EdgeSocket {
std::string inbox, sent;
void SendAll(const std::string& data) override { sent += data; }
std::string Recv(size_t max) override { std::string r = inbox.substr(0, max); inbox.erase(0, r.size()); return r; }
void Close() override {}
};
long gUuidCounter = 0;
void FixEnv(double now) {
gUuidCounter = 0;
EdgeSpeech::env().now = [now] { return now; };
EdgeSpeech::env().randomBytes = [](size_t n) { std::string o; for (size_t i = 0; i < n; ++i) o.push_back((char)((i * 7 + 1) % 256)); return o; };
EdgeSpeech::env().uuidHex = [] { char b[40]; std::snprintf(b, sizeof b, "%032lx", ++gUuidCounter); return std::string(b); };
}
struct RecordingStream : StreamSink {
void Feed(const std::string& line) override { Out("FEED " + J(line) + "\n"); }
void Pause() override { Out("PAUSE\n"); }
void Close(const std::string&) override {}
};
AgentRow RowFrom(const JsonValue& v) {
AgentRow r;
auto num = [&](const char* k) { const JsonValue* x = v.Get(k); return x ? x->Str() : std::string(); };
r.id = L(num("id"));
r.status = v.Get("status") ? v.Get("status")->Str() : "";
r.task = v.Get("task") && !v.Get("task")->is_null() ? v.Get("task")->Str() : "";
r.elapsed = D(num("elapsed"));
if (v.Get("tools") && !v.Get("tools")->is_null()) r.tools = L(num("tools"));
r.collected = v.Get("collected") && v.Get("collected")->Truthy();
return r;
}
} // namespace
int main(int argc, char** argv) {
std::vector<std::string> a(argv + 1, argv + argc);
if (a.empty()) return 2;
for (auto& item : a) { // arguments beyond the kernel's per-argument limit arrive as "@@FILE:<path>"
if (item.rfind("@@FILE:", 0) == 0) {
FILE* fh = std::fopen(item.c_str() + 7, "rb");
if (!fh) return 2;
item.clear();
char buf[65536];
size_t n;
while ((n = std::fread(buf, 1, sizeof buf, fh)) > 0) item.append(buf, n);
std::fclose(fh);
}
}
const std::string f = a[0];
auto arg = [&](size_t i) -> std::string { return i < a.size() ? a[i] : std::string(); };
if (f == "truncate") Out(Truncate(arg(1), L(arg(2)), L(arg(3))));
else if (f == "capped") {
Capped c = CappedText(arg(2) == "none" ? std::nullopt : std::optional<std::string>(arg(1)));
Out(std::string(c.present ? "1" : "0") + std::string(1, kRs) + std::to_string(c.size) + std::string(1, kRs) + c.text);
} else if (f == "chunk") Out(Join(ChunkLines(arg(1), (int)L(arg(2)), (int)L(arg(3)), (int)L(arg(4))), std::string(1, kRs)));
else if (f == "collapse") Out(CollapseRepeatedLines(arg(1), (int)L(arg(2))));
else if (f == "tags") { auto r = SplitLeadingTags(arg(1)); Out(Join(r.first, std::string(1, kUs)) + std::string(1, kRs) + r.second); }
else if (f == "mentions") Out(ExpandFileMentions(arg(1), arg(2)));
else if (f == "complete") Out(Join(CompleteFiles(arg(1), arg(2), (size_t)L(arg(3))), "\n"));
else if (f == "human_size") Out(HumanSize(L(arg(1))));
else if (f == "stream_status") Out(StreamStatus((int)L(arg(1)), arg(2), D(arg(3)), arg(4) == "1"));
else if (f == "duration") { long v = ParseDuration(arg(1)); Out(v < 0 ? "None" : std::to_string(v)); }
else if (f == "delay") Out(FormatDelay(L(arg(1))));
else if (f == "tokens") Out(std::to_string(EstimateTokens(arg(1))));
else if (f == "as_int") Out(std::to_string(AsInt(arg(1), L(arg(2)))));
else if (f == "toolname") Out(ToolNameClean(arg(1)));
else if (f == "target_paths") Out(Join(ShellTargets::Paths(arg(1), arg(2)), "\n"));
else if (f == "segment") Out(Join(ShellTargets::Segment(std::vector<std::string>(a.begin() + 1, a.end())), "\n"));
else if (f == "curl_hint") Out(CurlHint(arg(1)));
else if (f == "strip_ansi") Out(AnsiText::Strip(arg(1)));
else if (f == "wrap") Out(Join(AnsiText::Wrap(arg(1), L(arg(2)), arg(3), arg(4)), "\n"));
else if (f == "trunc_ansi") Out(AnsiText::Truncate(arg(1), L(arg(2))));
else if (f == "pad_tinted") Out(AnsiText::PadTinted(arg(1), L(arg(2))));
else if (f == "style_span") Out(AnsiText::StyleSpan(arg(1), {arg(2), arg(3)}));
else if (f == "md_inline") Out(Markdown::Inline(arg(1)));
else if (f == "markdown") Out(Markdown::Render(arg(1), L(arg(2)), 1));
else if (f == "diff") Out(DiffRender::Render(arg(1), Opt(arg(2)), Opt(arg(3)), L(arg(4)), arg(5) == "1"));
else if (f == "highlight") Out(PyHighlight::Line(arg(1)));
else if (f == "g_event") Out(Glow::EventRow(arg(1), arg(2), arg(3), arg(4), L(arg(5)), arg(6)));
else if (f == "g_done") Out(Glow::DoneRow(arg(1), arg(2), L(arg(3)), D(arg(4)), arg(5) == "1", L(arg(6))));
else if (f == "g_ellipsis") Out(Glow::Ellipsis(arg(1), L(arg(2)), arg(3) == "1"));
else if (f == "g_shimmer") Out(Glow::Shimmer(arg(1), L(arg(2)), L(arg(3))));
else if (f == "g_describe") {
CallDescription c = Glow::DescribeCall(arg(1), arg(2));
Out(J(c.verb) + "\n" + J(c.target) + "\n" + J(c.extra) + "\n" + J(c.preview));
} else if (f == "g_status") Out(Glow::Statusbar(arg(1), arg(2), arg(3), arg(4) == "1", L(arg(5)), L(arg(6))));
else if (f == "g_accent") Out(Glow::AccentFor(arg(1)) + "|" + Glow::Bar(arg(1)) + "|" + Glow::IconFor(arg(1)));
else if (f == "g_hair") Out(Glow::Hairline(L(arg(1))));
else if (f == "g_prompt") Out(Glow::PromptText());
else if (f == "g_preview") Out(Glow::PreviewRow(arg(1), L(arg(2))));
else if (f == "g_agent_row") {
bool ok; JsonValue v = JsonValue::Parse(arg(1), &ok);
Out(Glow::AgentRowText(RowFrom(v), L(arg(2)), arg(3), arg(4)));
} else if (f == "g_lanes") {
bool ok; JsonValue v = JsonValue::Parse(arg(1), &ok);
std::vector<AgentRow> rows;
for (auto& item : v.items()) rows.push_back(RowFrom(item));
Out(Join(Glow::Lanes(rows, L(arg(2)), L(arg(3)), arg(4)), "\n"));
} else if (f == "json") {
bool ok; JsonValue v = JsonValue::Parse(arg(1), &ok);
Out(ok ? v.Dump(false) + "\n" + v.Dump(true) + "\n" + v.Str() + "\n" + v.Repr() : "ERR");
} else if (f == "tool_args") Out(ToolArgs::Text(arg(1)) + "\n" + ToolArgs::Valid(arg(1)) + "\n" + ToolArgs::Signature("name", arg(1)));
else if (f == "md5") Out(ShellSession::Md5Hex(arg(1)));
else if (f == "sess_stat") {
auto st = ShellSession::ParseStat(arg(1));
Out(st.ok ? J(st.comm) + " " + std::to_string(st.ppid) + " " + std::to_string(st.ticks) : "None");
} else if (f == "sess_name") Out(ShellSession::ShellName(arg(1), arg(2)));
else if (f == "sess_boot") Out(ShellSession::BootTag(arg(1)));
else if (f == "sess_current") {
EnvMap env; if (!arg(1).empty()) env.Set("TAI_SESSION", arg(1));
ShellSession s = ShellSession::Current(&env, L(arg(2)), arg(3), D(arg(4)), L(arg(5)));
std::string chain;
for (auto& e : s.chain) chain += std::to_string(e.pid) + "," + std::to_string(e.ppid) + "," + e.comm + "," + e.argv0 + "," + e.name + "," + e.startIso + "," + e.sid + ";";
Out(J(s.sid) + "\n" + J(s.shell) + "\n" + std::to_string(s.pid) + "\n" + J(s.started) + "\n" + (s.parent ? J(*s.parent) : "null") + "\n" + chain);
} else if (f == "live") {
// live <height> <width> <line>... : LiveStream into a StringSink
StringSink sink;
LiveStream ls((int)L(arg(1)), (int)L(arg(2)), &sink, true);
for (size_t i = 4; i < a.size(); ++i) { if (a[i] == "\x01PAUSE") ls.Pause(); else ls.Feed(a[i]); }
ls.Close(arg(3));
Out(sink.str());
} else if (f == "run") {
// run <timeout> <shell|argv> <ask:0|1> <answers...> -- command... (answers: text, or "\001DECLINE")
double timeout = D(arg(1));
bool useShell = arg(2) == "shell";
bool ask = arg(3) == "1";
size_t dash = 4;
std::vector<std::string> answers;
while (dash < a.size() && a[dash] != "--") answers.push_back(a[dash++]);
std::vector<std::string> cmd(a.begin() + (long)dash + 1, a.end());
RecordingStream stream;
RunLiveOptions opt;
opt.timeout = timeout;
opt.stream = &stream;
size_t next = 0;
if (ask) opt.ask = [&](const std::string& text, bool secret) -> std::optional<std::string> {
Out("ASK " + J(text) + " " + (secret ? "secret" : "plain") + "\n");
if (next >= answers.size()) return std::nullopt;
std::string ans = answers[next++];
if (ans.rfind("\001SLEEP", 0) == 0) { // "\001SLEEP<ms>:<answer>": the user takes a while to answer
size_t colon = ans.find(':');
usleep((useconds_t)L(ans.substr(6, colon - 6)) * 1000);
ans = ans.substr(colon + 1);
}
if (ans == "\001DECLINE") return std::nullopt;
return ans;
};
try {
RunResult r = RunLive::Run(useShell ? Command::Shell(cmd[0]) : Command::Argv(cmd), opt);
Out("RESULT " + std::to_string(r.code) + " " + (r.timedOut ? "timeout" : "ok") + " " + J(r.output) + "\n");
} catch (const std::exception& e) {
Out(std::string("ERROR ") + J(e.what()) + "\n");
}
} else if (f == "box") {
::dup2(1, 2); // the Python side captures stdout and stderr into one stream
const std::string sc = arg(1), engine = arg(2);
auto text = [](const std::string& bytes) { return Utf8::Encode(Utf8::Decode(bytes)); };
try {
if (sc == "engine") Out(Sandbox::ContainerEngine());
else if (sc == "state") Out(Sandbox::BoxState(engine));
else if (sc == "ensure") Out("OK " + Sandbox::EnsureBox());
else if (sc == "caps") Out(Sandbox::UpdateBoxCaps(engine) ? "True" : "False");
else if (sc == "python") Out(Sandbox::BoxPython(engine));
else if (sc == "stt") Out("OK " + Sandbox::BoxTranscribe(engine, arg(3)));
else if (sc == "tts") Out("OK " + ToHex(Sandbox::BoxSpeak(engine, arg(3), arg(4))));
else if (sc == "ocbin") Out(Sandbox::BoxOpencodeBinary(engine) ? "True" : "False");
else if (sc == "ocrun") {
CompletedProcess d = Sandbox::BoxOpencodeExec(engine, arg(3), arg(4), D(arg(5)), "build");
Out("PARSE|" + std::to_string(d.returncode) + "|" + text(d.out) + "|" + text(d.err));
} else if (sc == "exec") {
CompletedProcess d = Sandbox::BoxExec(engine, std::vector<std::string>(a.begin() + 3, a.end()));
Out("RC " + std::to_string(d.returncode) + " " + J(text(d.out)) + " " + J(text(d.err)));
}
} catch (const BackendError& e) { Out(std::string("ERR ") + e.what());
} catch (const TimeoutExpired& e) { Out(std::string("TIMEOUT ") + e.what());
} catch (const OsError& e) { Out(std::string("OSERR ") + e.what());
}
} else if (f == "edge_token") { FixEnv(D(arg(2))); Out(EdgeSpeech::Token(L(arg(1))));
} else if (f == "edge_ts") { FixEnv(D(arg(1))); Out(EdgeSpeech::Timestamp());
} else if (f == "edge_ssml") Out(EdgeSpeech::Ssml(arg(1), arg(2)));
else if (f == "edge_chunks") Out(Join(EdgeSpeech::Chunks(arg(1), (size_t)L(arg(2))), std::string(1, kRs)));
else if (f == "ws_send") { FixEnv(0); MemorySocket m; EdgeSpeech::WsSend(m, arg(1)); Out(ToHex(m.sent));
} else if (f == "ws_recv") {
FixEnv(0); MemorySocket m; m.inbox = FromHex(arg(1));
try { auto r = EdgeSpeech::WsRecv(m); Out("OK " + std::to_string(r.first) + " " + ToHex(r.second)); }
catch (const BackendError& e) { Out(std::string("ERR ") + e.what()); }
Out("|sent=" + ToHex(m.sent));
} else if (f == "edge_skew") { FixEnv(D(arg(2))); Out(std::to_string(EdgeSpeech::Skew(arg(1))));
} else if (f == "edge_session") {
FixEnv(D(arg(4))); MemorySocket m; m.inbox = FromHex(arg(1));
try { std::string audio = EdgeSpeech::SynthesizeOn(m, arg(2), arg(3)); Out("OK " + ToHex(audio)); }
catch (const BackendError& e) { Out(std::string("ERR ") + e.what()); }
Out("|sent=" + ToHex(m.sent));
} else if (f == "edge_live") {
try { std::string audio = EdgeSpeech::Synthesize(arg(1), arg(2)); Out("OK " + std::to_string(audio.size()) + " " + ToHex(audio.substr(0, 3))); }
catch (const std::exception& e) { Out(std::string("ERR ") + e.what()); }
} else if (f == "listen") Out(Audio::Listen(L(arg(1)), arg(2)));
else if (f == "play") Out(Audio::Play(arg(1)) ? "True" : "False");
else if (f == "spinner") {
// spinner <label> <ms>: stderr output only appears when stderr is a tty
Spinner sp(arg(1));
sp.Start();
usleep((useconds_t)L(arg(2)) * 1000);
sp.Stop();
} else if (f == "lane") {
// lane <json: list of record lists, one per paint> <width> <cap>
bool ok; JsonValue v = JsonValue::Parse(arg(1), &ok);
std::vector<std::vector<AgentRow>> seq;
for (auto& rows : v.items()) { std::vector<AgentRow> r; for (auto& item : rows.items()) r.push_back(RowFrom(item)); seq.push_back(r); }
size_t call = 0;
StringSink sink;
LaneBoard board("prof", [&](const std::string&) { return seq[std::min(call++, seq.size() - 1)]; }, (int)L(arg(2)), (int)L(arg(3)), &sink);
board.Start();
board.Stop();
Out(sink.str() + "\npainted=" + std::to_string(board.painted()));
} else { std::fprintf(stderr, "unknown function %s\n", f.c_str()); return 2; }
return 0;
}