Files
tai/taicpp/tests/agent_e2e.cpp
T
retoorandClaude Opus 4.8 115e403e67 whatsapp: native C++ client + terminal-screenshot tool
Native WhatsApp Web multi-device client in C++ (src/whatsapp/, namespace
detz::wa), built on OpenSSL plus a vendored public-domain ed25519 ref10
(third_party/ed25519, zlib) for XEdDSA and the nayuki QR generator
(third_party/qrcodegen, MIT); no new runtime dependency.

- WaCrypto, WaBinary, WaProto, WaNoise, WaSignal, WaDevice, WaSocket,
  WaClient, WaStore, WaQr, WaWizard.
- Verified offline against golden vectors captured from the real Baileys
  client (tests/whatsapp_data, from jfelipesjc/wa-go, MIT): HKDF/PBKDF2 RFC
  vectors, XEdDSA roundtrips, codec battery 19/19, ClientPayload byte-exact,
  the Noise handshake, and a 4-exchange Signal session byte-exact. Unit test
  build/whatsapp_test (16/16), run_all step whatsapp.unit.
- Verified live against web.whatsapp.com: `detz --whatsapp-link` connects,
  completes the Noise handshake, receives pair-device, renders a scannable QR
  in the terminal, and persists the device to the vault (whatsapp/<profile>.db).
  Also --whatsapp-status / --whatsapp-unlink.
- Agent tools whatsapp_send and whatsapp_contacts (detz-only, in DetzTools).
- Pairing completion after the phone scans, and the message send path
  (usync + prekey fetch + Signal encrypt + send node), are ported faithfully
  from whatsmeow but unverified in-tree (no linked account in development).
  send_image and own-device fan-out are not wired yet. See docs/whatsapp.md.

Also in this snapshot: the terminal-screenshot tool (src/termshot/,
terminal_screenshot, docs/termshot.md, research/terminal-screenshot-rendering)
and a concurrent session's in-progress llm/muse failover changes carried along
in the shared files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-27 18:22:24 +01:00

275 lines
11 KiB
C++

// End-to-end driver: runs one scenario (JSON file) through the C++ Agent with a
// scripted HTTP transport, an in-memory store and a popen shell, then prints
// the observable outcome as JSON. tests/agent_e2e.py runs the same scenario
// through tai.py and compares.
#include "../src/agent/agent.h"
#include "../src/agent/detz_tools.h"
#include "../src/llm/http_transport.h"
#include "../src/llm/py.h"
#include "agent_memstore.h"
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <unistd.h>
using namespace detz;
using json::Value;
namespace fs = std::filesystem;
namespace {
struct ScriptExhausted {}; // deliberately not a std::exception: must not be retried
class ScriptedTransport : public llm::HttpTransport {
public:
ScriptedTransport(Value script, Value single) : script_(std::move(script)), single_(std::move(single)) {}
Value requests = Value::array();
llm::HttpResult send(const llm::HttpRequest& req) override {
if (req.url.size() >= 7 && req.url.compare(req.url.size() - 7, 7, "/models") == 0) {
llm::HttpResult r;
r.error = "<urlopen error fake>";
return r;
}
record(req);
const Value& item = next(single_, single_pos_);
return reply(item, nullptr);
}
llm::HttpResult stream(const llm::HttpRequest& req, const llm::LineSink& sink) override {
record(req);
const Value& item = next(script_, script_pos_);
return reply(item, &sink);
}
private:
void record(const llm::HttpRequest& req) {
Value r = Value::object();
r.set("method", req.method);
r.set("url", req.url);
Value body;
if (req.body.has_value()) {
try { body = Value::loads(*req.body); } catch (const json::ParseError&) { body = Value(*req.body); }
}
r.set("body", body);
requests.push(r);
}
const Value& next(const Value& queue, size_t& pos) {
if (pos >= queue.size()) { throw ScriptExhausted{}; }
return queue.at(pos++);
}
llm::HttpResult reply(const Value& item, const llm::LineSink* sink) {
llm::HttpResult r;
std::string kind = item.get("kind").str();
if (kind == "transport_error") { r.error = "<urlopen error scripted>"; return r; }
if (kind == "status") {
r.status = item.get("status").as_int();
r.body = item.get("body").str();
return r;
}
r.status = 200;
if (item.get("content_type").is_string()) { r.content_type = item.get("content_type").str(); }
if (kind == "json") {
r.body = item.get("body").dumps();
return r;
}
if (kind == "raw") {
r.body = item.get("body").str();
return r;
}
// stream: SSE lines
if (sink == nullptr) { throw std::runtime_error("stream script item consumed by a non-stream call"); }
for (const auto& ev : item.get("events").items()) {
if (!(*sink)("data: " + ev.dumps() + "\n")) { return r; }
if (!(*sink)("\n")) { return r; }
}
(*sink)("data: [DONE]\n");
return r;
}
Value script_, single_;
size_t script_pos_{0}, single_pos_{0};
};
class PopenShell : public agent::ShellPort {
public:
agent::RunResult run_live_shell(const std::string& command, const std::string& cwd, const std::map<std::string, std::string>& extra_env,
agent::LiveStreamPort*, const agent::AskFn&) override {
std::string env;
for (const auto& kv : extra_env) { env += "export " + kv.first + "='" + kv.second + "'; "; }
std::string full = "cd '" + cwd + "' && " + env + command + " 2>&1";
FILE* pipe = popen(("/bin/sh -c '" + escape(full) + "' < /dev/null").c_str(), "r");
agent::RunResult r;
if (pipe == nullptr) { r.code = 127; return r; }
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof buf, pipe)) > 0) { r.output.append(buf, n); }
int status = pclose(pipe);
r.code = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
r.output = py::utf8_replace(r.output);
return r;
}
agent::RunResult run_live_argv(const std::vector<std::string>&, agent::LiveStreamPort*, const agent::AskFn&) override {
throw agent::PortNotWired("shell argv");
}
std::unique_ptr<agent::LiveStreamPort> make_live_stream() override { return nullptr; }
std::vector<std::string> shell_target_paths(const std::string&, const std::string&) override { return {}; }
std::string ensure_box() override { throw llm::BackendError("no container engine (test)"); }
llm::ProcResult box_exec(const std::string&, const std::vector<std::string>&, const std::optional<std::string>&, double) override {
throw agent::PortNotWired("box");
}
std::unique_ptr<agent::LaneBoardPort> make_lane_board(const std::string&) override { return nullptr; }
private:
static std::string escape(const std::string& s) {
std::string out;
for (char c : s) {
if (c == '\'') { out += "'\\''"; } else { out += c; }
}
return out;
}
};
class FakeSkills : public agent::SkillsPort {
public:
explicit FakeSkills(std::string catalog) : catalog_(std::move(catalog)) {}
Value discover_skills(const std::string&, const std::string&) override { return Value::object(); }
std::string skill_catalog(const Value&) override { return catalog_; }
std::string skill_provenance_note(const Value&) override { return ""; }
std::vector<std::string> skill_extra_files(const std::string&) override { return {}; }
std::optional<Value> skill_blueprint(const std::string&) override { return std::nullopt; }
std::string skill_scope_dir(const std::string&, const std::string&, const std::string&, const std::string&) override { return ""; }
std::string default_skill_scope(const std::string&) override { return "project"; }
Value parse_skill_file(const std::string&) override { return Value(); }
bool is_external_skill_root(const std::string&) override { return false; }
void save_skill_install_meta(const std::string&, const std::string&, const Value&) override {}
private:
std::string catalog_;
};
std::string slurp(const std::string& path) {
std::ifstream in(path, std::ios::binary);
std::stringstream ss;
ss << in.rdbuf();
return ss.str();
}
} // namespace
int main(int argc, char** argv) {
agent::DetzTools::set_enabled(false); // compare the tai.py tool set only
if (argc < 3) { std::fprintf(stderr, "usage: agent_e2e scenario.json workdir\n"); return 2; }
Value sc = Value::loads(slurp(argv[1]));
std::string workdir = argv[2];
if (chdir(workdir.c_str()) != 0) { std::perror("chdir"); return 2; }
std::string home = workdir + "/.home";
fs::create_directories(home);
agent::AgentConfig cfg;
cfg.home = home;
cfg.audio_dir = home + "/audio";
cfg.profile = sc.get("profile").is_string() ? sc.get("profile").str() : "default";
cfg.auto_approve = sc.get("auto_approve").truthy();
cfg.retry_seconds = sc.get("retry_seconds").truthy() ? sc.get("retry_seconds").as_double() : 0.0;
cfg.opencode_enabled = false;
cfg.openrouter_enabled = false;
cfg.pollinations_enabled = false;
cfg.zen_enabled = false;
llm::BackendSpec b;
b.label = "lab"; b.base = "http://fake.invalid/v1"; b.key = "k"; b.model = "m1"; b.profile = "";
cfg.backends.push_back(b);
ScriptedTransport transport(sc.get("script"), sc.get("single"));
llm::set_transport(&transport);
agent::testing::MemStore store;
PopenShell shell;
FakeSkills skills(sc.get("skill_catalog").str_or(""));
agent::ports().shell = &shell;
agent::ports().skills = &skills;
llm::ChatClient::sleeper() = [](double) {};
if (sc.get("presets").is_object()) {
// pre-existing vault state: systems and secrets
for (const auto& kv : sc.get("presets").get("systems").members()) { store.save_system(kv.first, kv.second.str()); }
for (const auto& kv : sc.get("presets").get("secrets").members()) {
store.save_secret(kv.first, kv.second.get("value").str(), Value::object(), {}, "default");
}
for (const auto& kv : sc.get("presets").get("bots").members()) {
store.save_bot_system("default", kv.first, kv.second.get("system").str());
Value bots = store.load_bots("default");
Value e = Value::object();
e.set("nicknames", kv.second.get("nicknames"));
e.set("created", "t");
e.set("updated", "t");
bots.set(kv.first, e);
store.save_bots("default", bots);
}
}
agent::Agent ag(cfg, &store, sc.get("persist").is_bool() ? sc.get("persist").as_bool() : true, true, 0, false, sc.get("yolo").truthy());
if (sc.get("deadline_expired").truthy()) { ag.deadline = 1.0; }
Value answers = sc.get("answers");
size_t answer_pos = 0;
if (answers.is_array()) {
ag.prompter = [&](const std::string&, const std::string&, const std::vector<std::string>&) -> std::optional<std::string> {
if (answer_pos >= answers.size()) { return std::nullopt; }
const Value& a = answers.at(answer_pos++);
if (a.is_null()) { return std::nullopt; }
return a.str();
};
}
Value runner_script = sc.get("runner_script");
size_t runner_pos = 0;
if (runner_script.is_array()) {
ag.runner_override = [&](const std::string&, const std::string&, double) -> std::string {
if (runner_pos >= runner_script.size()) { return "runner exhausted"; }
return runner_script.at(runner_pos++).str();
};
}
Value out = Value::object();
try {
Value turns = sc.get("prompts").is_array() ? sc.get("prompts") : Value::array();
if (turns.size() == 0) { turns.push(sc.get("prompt")); }
Value results = Value::array();
for (const auto& p : turns.items()) {
results.push(ag.run_turn(p.str(), true, sc.get("max_steps").truthy() ? static_cast<int>(sc.get("max_steps").as_int()) : agent::MAX_STEPS));
}
out.set("results", results);
if (sc.get("verify").truthy()) {
auto [text, ok] = ag.verify_loop(sc.get("prompt").str(), results.at(results.size() - 1).str());
Value v = Value::object();
v.set("text", text);
v.set("ok", ok);
out.set("verify", v);
}
} catch (const ScriptExhausted&) {
out.set("exception", "script exhausted");
} catch (const std::exception& e) {
out.set("exception", std::string(e.what()));
}
out.set("messages", ag.messages);
out.set("requests", transport.requests);
Value events = Value::array();
for (const auto& e : store.events) {
Value row = Value::array();
row.push(e.role); row.push(e.kind); row.push(e.text);
events.push(row);
}
out.set("events", events);
Value audits = Value::array();
for (const auto& a : store.audits) {
Value row = Value::object();
row.set("actor", a.actor); row.set("action", a.action); row.set("path", a.path); row.set("message", a.message);
row.set("tags", a.tags);
row.set("old_size", a.old_size ? Value(*a.old_size) : Value());
row.set("new_size", a.new_size ? Value(*a.new_size) : Value());
row.set("old", a.old_text ? Value(*a.old_text) : Value());
row.set("new", a.new_text ? Value(*a.new_text) : Value());
audits.push(row);
}
out.set("audits", audits);
out.set("tool_calls", ag.tools.calls);
out.set("yolo", ag.yolo);
out.set("timed_out", ag.timed_out);
out.set("system", ag.system_message);
std::cout << out.dumps() << "\n";
return 0;
}