1667 lines
79 KiB
C++
1667 lines
79 KiB
C++
// C++ side of the differential tests (tests/misc_diff.py): prints the same transcript as tests/misc_py.py.
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <iostream>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#include <map>
|
|
#include <sstream>
|
|
#include "bundled.h"
|
|
#include "bundled_data.h"
|
|
#include "audittool.h"
|
|
#include "release.h"
|
|
#include "chunker.h"
|
|
#include "telegram.h"
|
|
#include "ragtool.h"
|
|
#include "retrievaleval.h"
|
|
#include "install.h"
|
|
#include "installconsts.h"
|
|
#include "ftssearch.h"
|
|
#include "miscdatetime.h"
|
|
#include "miscjson.h"
|
|
#include "miscpy.h"
|
|
#include "miscsha256.h"
|
|
#include "schedtime.h"
|
|
#include "schedtool.h"
|
|
#include "schedvault.h"
|
|
#include "sqlitedb.h"
|
|
#include "scheduler.h"
|
|
#include "schedtool.h"
|
|
#include "skillfile.h"
|
|
#include "skilltool.h"
|
|
using detz::misc::Json;
|
|
using detz::misc::PyFs;
|
|
using detz::misc::PyStr;
|
|
namespace sk = detz::skills;
|
|
|
|
static std::string DATA_DIR;
|
|
|
|
static Json data(const std::string& name) {
|
|
std::ifstream in(DATA_DIR + "/" + name, std::ios::binary);
|
|
std::stringstream ss;
|
|
ss << in.rdbuf();
|
|
return Json::loads(ss.str());
|
|
}
|
|
static std::string j(const Json& v) { return v.dumps(); }
|
|
static std::string js(const std::string& s) { return Json(s).dumps(); }
|
|
static Json arr_of(const std::vector<std::string>& v) {
|
|
Json a = Json::array();
|
|
for (const auto& s : v) a.push(s);
|
|
return a;
|
|
}
|
|
static void out(const std::string& line = "") { std::cout << line << "\n"; }
|
|
static std::string tai_home() { return PyFs::env("TAI_HOME"); }
|
|
static std::string py_bool(bool b) { return b ? "True" : "False"; }
|
|
static void write(const std::string& path, const std::string& text) {
|
|
PyFs::makedirs(PyFs::dirname(path));
|
|
PyFs::write_bytes(path, text);
|
|
}
|
|
static std::string read(const std::string& path) { return PyFs::read_bytes(path); }
|
|
|
|
// ------------------------------------------------------------------ scenarios without state
|
|
static void sc_json(const std::string&) {
|
|
for (const auto& c : data("json_cases.json").items()) {
|
|
try {
|
|
Json v = Json::loads(c.as_string());
|
|
out(js(c.as_string()) + " => " + v.dumps());
|
|
} catch (const detz::misc::JsonError&) {
|
|
out(js(c.as_string()) + " => ERR");
|
|
}
|
|
}
|
|
}
|
|
|
|
static void sc_datetime(const std::string&) {
|
|
for (const auto& c : data("datetime_cases.json").items()) {
|
|
std::string iso, parsed;
|
|
try {
|
|
iso = detz::misc::DateTime::fromisoformat(c.as_string()).isoformat();
|
|
} catch (const std::exception&) {
|
|
iso = "ERR";
|
|
}
|
|
try {
|
|
parsed = detz::sched::ScheduleTime::parse_schedule_at(c.as_string());
|
|
} catch (const std::exception& e) {
|
|
parsed = std::string("ERR ") + e.what();
|
|
}
|
|
bool err = parsed.rfind("ERR", 0) == 0;
|
|
std::string shown = "-";
|
|
if (!err) {
|
|
try {
|
|
shown = js(detz::sched::ScheduleTime::local_display(parsed));
|
|
} catch (const std::exception& e) {
|
|
shown = std::string("ERR ") + e.what();
|
|
}
|
|
}
|
|
out(js(c.as_string()) + " => " + iso + " | " + parsed + " | " + shown);
|
|
}
|
|
for (const auto& c : data("duration_cases.json").items()) {
|
|
auto d = detz::sched::ScheduleTime::parse_duration(c.as_string());
|
|
out("duration " + js(c.as_string()) + " => " + (d ? std::to_string(*d) : std::string("null")));
|
|
}
|
|
for (const auto& c : data("delay_cases.json").items()) {
|
|
out("delay " + j(c) + " => " + js(detz::sched::ScheduleTime::format_delay(c.as_double())));
|
|
}
|
|
}
|
|
|
|
static void sc_chunk(const std::string&) {
|
|
for (const auto& c : data("chunk_cases.json").items()) {
|
|
std::string text = c.get("text")->as_string();
|
|
auto chunks = detz::rag::Chunker::chunk_lines(text, static_cast<int>(c.get("max_lines")->as_int()), static_cast<int>(c.get("overlap")->as_int()),
|
|
static_cast<int>(c.get("max_chars")->as_int()));
|
|
out("chunks " + std::to_string(chunks.size()) + " " + j(arr_of(chunks)));
|
|
out("collapse " + js(detz::rag::Chunker::collapse_repeated_lines(text)));
|
|
out("lines " + j(arr_of(PyStr::splitlines(text))));
|
|
out("strip " + js(PyStr::strip(text)));
|
|
}
|
|
}
|
|
|
|
static void sc_fts(const std::string&) {
|
|
using detz::rag::FtsQuery;
|
|
for (const auto& c : data("fts_cases.json").items()) {
|
|
auto terms = FtsQuery::fts_escape(c.as_string());
|
|
out(js(c.as_string()) + " => " + (terms ? j(arr_of(*terms)) : std::string("null")));
|
|
if (terms) {
|
|
out(" match " + js(FtsQuery::fts_match(*terms)));
|
|
out(" or " + js(FtsQuery::fts_match_or(*terms)));
|
|
out(" prefix " + js(FtsQuery::fts_match_prefix(*terms)));
|
|
out(" variants " + j(arr_of(FtsQuery::fts_match_variants(*terms))));
|
|
}
|
|
}
|
|
out("rank " + js(FtsQuery::fts_rank("fts_docs")));
|
|
for (const std::string& raw : std::vector<std::string>{"Release Notes!", " a b ", "---", std::string(40, 'x'), "\xc3\xa9t\xc3\xa9", "a,b;c", "Hello World 2", "..", "UPPER", "a-b-c"}) {
|
|
out("tag " + js(raw) + " => " + js(detz::misc::TagNames::normalize_tag(raw)));
|
|
}
|
|
std::vector<std::string> items = {"A b", "a-b", "", "C!", std::string(40, 'd'), std::string(39, 'd') + "e"};
|
|
for (int i = 0; i < 30; ++i) items.push_back("t" + std::to_string(i));
|
|
out("tags " + j(arr_of(detz::misc::TagNames::normalize_tags(items))));
|
|
}
|
|
|
|
static void sc_skills_data(const std::string&) {
|
|
for (const auto& kv : sk::BundledData::files()) {
|
|
out("skill " + kv.first + " upstream " + js(sk::BundledData::upstream().at(kv.first)));
|
|
for (const auto& f : kv.second) {
|
|
out(" file " + f.first + " len " + std::to_string(f.second.size()) + " sha8 " + sk::BundledSkills::bundled_skill_digest(f.second) +
|
|
" sha256 " + detz::misc::Sha256::hex(f.second));
|
|
}
|
|
}
|
|
for (const auto& kv : sk::BundledData::blueprints()) {
|
|
out("blueprint " + kv.first + " hint " + js(kv.second.hint) + " scope " + kv.second.scope + " brief " + js(kv.second.brief));
|
|
}
|
|
out("catalog empty " + js(sk::SkillFiles::skill_catalog(sk::SkillSet())));
|
|
sk::SkillSet two;
|
|
sk::Skill a;
|
|
a.name = "deep-research";
|
|
a.description = std::string(400, 'x');
|
|
sk::Skill b;
|
|
b.name = "zeta";
|
|
b.description = "short";
|
|
two.put(a);
|
|
two.put(b);
|
|
out("catalog one " + js(sk::SkillFiles::skill_catalog(two)));
|
|
out("prompt " + js(sk::SkillFiles::create_skill_prompt("my-skill", "do the thing", "/tmp/x/my-skill")));
|
|
for (const std::string& raw : std::vector<std::string>{"a", "ab", "a-b", "-a", "a-", "A", "a_b", std::string(64, 'a'), std::string(65, 'a'), "", "a b", "0x", "a\n"}) {
|
|
out("name " + js(raw) + " => " + py_bool(sk::SkillFiles::skill_name_valid(raw)));
|
|
}
|
|
std::vector<std::vector<std::string>> front_cases = {
|
|
{"name: x", "description: y"},
|
|
{"name: 'x'", "description: \"y z\""},
|
|
{"name: x", "description: >", " line one", " line two", "other: v"},
|
|
{"name: x", "description: |", " line one", " line two", "tail: 1"},
|
|
{"name: x", "description: >-", " folded"},
|
|
{" indented: no", "key: va:lue", "noval", "k2:", "k3: ''quoted''"},
|
|
{"a: >", "b: 1"},
|
|
{"a: |", "\tt1", " t2", "z: 9"},
|
|
};
|
|
for (const auto& lines : front_cases) {
|
|
auto front = sk::SkillFiles::parse_skill_front(lines);
|
|
// json.dumps of a dict keeps the parse (insertion) order; the C++ map is sorted, so compare as sorted key objects
|
|
Json o = Json::object();
|
|
for (const auto& kv : front) o.set(kv.first, kv.second);
|
|
out("front " + j(arr_of(lines)) + " => " + j(o));
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ filesystem scenarios
|
|
static Json skill_json(const std::optional<sk::Skill>& s) {
|
|
if (!s) return Json();
|
|
Json o = Json::object();
|
|
o.set("name", s->name);
|
|
o.set("description", s->description);
|
|
o.set("body", s->body);
|
|
return o;
|
|
}
|
|
|
|
static void sc_skills_files(const std::string& root) {
|
|
std::string home = tai_home();
|
|
out("== front-matter parse");
|
|
std::vector<std::pair<std::string, std::string>> cases = {
|
|
{"ok", "---\nname: ok\ndescription: fine one\n---\n\n\nbody here\n"},
|
|
{"crlf", "---\r\nname: crlf\r\ndescription: win\r\n---\r\nbody\r\n"},
|
|
{"nofront", "just text\n"},
|
|
{"noend", "---\nname: noend\ndescription: d\n"},
|
|
{"badname", "---\nname: Bad_Name\ndescription: d\n---\nbody\n"},
|
|
{"nodesc", "---\nname: nodesc\n---\nbody\n"},
|
|
{"quoted", "---\nname: 'quoted'\ndescription: \"has: colon\"\n---\nbody\n"},
|
|
{"folded", "---\nname: folded\ndescription: >\n one\n two\n---\nbody\n"},
|
|
{"emptybody", "---\nname: emptybody\ndescription: d\n---"},
|
|
};
|
|
for (const auto& kv : cases) {
|
|
std::string path = root + "/parse/" + kv.first + "/SKILL.md";
|
|
write(path, kv.second);
|
|
out("parse " + kv.first + " => " + j(skill_json(sk::SkillFiles::parse_skill_file(path))));
|
|
}
|
|
out("== discover with external roots and collisions");
|
|
std::string proj = root + "/proj";
|
|
std::string uh = PyFs::home();
|
|
write(uh + "/.claude/skills/ext-home/SKILL.md", "---\nname: ext-home\ndescription: from claude home\n---\nbody ext-home\n");
|
|
write(uh + "/.claude/skills/shared/SKILL.md", "---\nname: shared\ndescription: external copy\n---\nbody external\n");
|
|
write(proj + "/.claude/skills/proj-claude/SKILL.md", "---\nname: proj-claude\ndescription: project claude\n---\nbody pc\n");
|
|
write(proj + "/.agents/skills/proj-agents/SKILL.md", "---\nname: proj-agents\ndescription: project agents\n---\nbody pa\n");
|
|
write(home + "/skills/shared/SKILL.md", "---\nname: shared\ndescription: tai copy\n---\nbody tai\n");
|
|
write(home + "/skills/native/SKILL.md", "---\nname: native\ndescription: native home skill\n---\nbody native\n");
|
|
write(home + "/skills/native/scripts/run.sh", "echo hi\n");
|
|
write(home + "/skills/native/references/ref.md", "ref\n");
|
|
write(home + "/skills/native/assets/a.txt", "a\n");
|
|
write(proj + "/.tai/skills/proj-tai/SKILL.md", "---\nname: proj-tai\ndescription: project tai skill\n---\nbody ptai\n");
|
|
write(home + "/skills/broken/SKILL.md", "no front matter\n");
|
|
write(home + "/skills/stray-file.txt", "x\n");
|
|
{
|
|
Json meta = Json::object();
|
|
meta.set("source", "/some/path");
|
|
meta.set("mode", "symlink");
|
|
meta.set("scope", "home");
|
|
meta.set("n", 3);
|
|
Json nested = Json::object();
|
|
Json arr = Json::array();
|
|
arr.push(1).push(Json()).push(true);
|
|
nested.set("a", arr);
|
|
meta.set("nested", nested);
|
|
sk::SkillFiles::save_skill_install_meta(home + "/skills", "native", meta);
|
|
}
|
|
auto skills = sk::SkillFiles::discover_skills(home, proj);
|
|
for (const auto& s : skills.items()) {
|
|
out("skill " + s.name + " root=" + s.root + " managed=" + py_bool(s.managed) + " install=" + (s.install ? j(*s.install) : std::string("null")) +
|
|
" note=" + js(sk::SkillFiles::skill_provenance_note(s)) + " extras=" + j(arr_of(sk::SkillFiles::skill_extra_files(s.root))));
|
|
out(" desc=" + js(s.description) + " body=" + js(s.body));
|
|
}
|
|
std::vector<std::string> order;
|
|
for (const auto& s : skills.items()) order.push_back(s.name);
|
|
out("order " + j(arr_of(order)));
|
|
out("catalog " + js(sk::SkillFiles::skill_catalog(skills)));
|
|
out("== scope helpers");
|
|
for (const std::string scope : {"project", "home"}) {
|
|
out("scope_dir " + scope + " " + sk::SkillFiles::skill_scope_dir(scope, home, proj, "nm"));
|
|
out("scope_dir " + scope + " " + sk::SkillFiles::skill_scope_dir(scope, home, proj));
|
|
}
|
|
std::string empty = root + "/emptyproj";
|
|
PyFs::makedirs(empty);
|
|
out("default scope empty " + sk::SkillFiles::default_skill_scope(empty));
|
|
for (const std::string marker : {".git", ".tai", ".claude/skills", ".agents/skills"}) {
|
|
std::string tag = PyStr::replace_all(marker, "/", "_");
|
|
std::string d = root + "/mk-" + tag;
|
|
PyFs::makedirs(d + "/" + marker);
|
|
out("default scope " + marker + " " + sk::SkillFiles::default_skill_scope(d));
|
|
}
|
|
for (const std::string path : {"/a/b/.claude/skills/x", "/a/b/.agents/skills/x/", "/a/b/other/x", "/a/.claude/skills/../x", "/a/.claude/skills//x"}) {
|
|
out("external " + path + " " + py_bool(sk::SkillFiles::is_external_skill_root(path)));
|
|
}
|
|
auto none = sk::SkillFiles::load_skill_install_meta(home + "/skills", "nothing");
|
|
out("meta none " + (none ? j(*none) : std::string("null")));
|
|
write(home + "/skills/.installs/bad.json", "{not json");
|
|
auto bad = sk::SkillFiles::load_skill_install_meta(home + "/skills", "bad");
|
|
out("meta bad " + (bad ? j(*bad) : std::string("null")));
|
|
auto ok = sk::SkillFiles::load_skill_install_meta(home + "/skills", "native");
|
|
out("meta ok " + (ok ? j(*ok) : std::string("null")));
|
|
}
|
|
|
|
static int count_files(const std::string& base) { return static_cast<int>(PyFs::walk_files(base).size()); }
|
|
|
|
static void sc_skills_install_tool(const std::string& root);
|
|
|
|
static void sc_skills_ops(const std::string& root) {
|
|
std::string home = tai_home();
|
|
sk::BundledSkills bundled;
|
|
auto step = [&](const std::string& label, const std::string& mode) {
|
|
out(label + " => " + bundled.op_skills(mode, home));
|
|
out(" status: " + sk::BundledSkills::status_line(home));
|
|
out(" files: " + std::to_string(count_files(home + "/skills")));
|
|
};
|
|
out("-- fresh");
|
|
step("install", "install");
|
|
step("install again", "install");
|
|
step("upgrade clean", "upgrade");
|
|
{
|
|
std::string s = "[";
|
|
bool first = true;
|
|
for (const auto& n : sk::BundledSkills::names()) {
|
|
s += std::string(first ? "" : ", ") + (sk::BundledSkills::bundled_skill_pristine(home, n) ? "true" : "false");
|
|
first = false;
|
|
}
|
|
out("pristine all " + s + "]");
|
|
}
|
|
std::string taste = home + "/skills/taste/SKILL.md";
|
|
write(taste, "user rewrite");
|
|
out("-- modified taste");
|
|
step("install", "install");
|
|
step("upgrade", "upgrade");
|
|
std::string extra = home + "/skills/taste/mynotes.md";
|
|
write(extra, "mine");
|
|
step("reinstall", "reinstall");
|
|
out("extra kept " + py_bool(PyFs::isfile(extra)));
|
|
out("-- crlf edit counts as equal after universal newlines");
|
|
std::string path = home + "/skills/plan/SKILL.md";
|
|
write(path, PyStr::replace_all(read(path), "\n", "\r\n"));
|
|
step("upgrade crlf", "upgrade");
|
|
out("-- uninstall with user content");
|
|
std::string gitp = home + "/skills/git/SKILL.md";
|
|
write(gitp, "user rewrite git");
|
|
std::string gextra = home + "/skills/git/mynotes.md";
|
|
write(gextra, "mine");
|
|
write(home + "/skills/git/sub/deep.txt", "deep");
|
|
step("uninstall", "uninstall");
|
|
step("uninstall again", "uninstall");
|
|
PyFs::remove(gitp);
|
|
PyFs::remove(gextra);
|
|
PyFs::remove(home + "/skills/git/sub/deep.txt");
|
|
step("uninstall final", "uninstall");
|
|
step("uninstall empty", "uninstall");
|
|
out("-- sidecar-only uninstall");
|
|
{
|
|
Json meta = Json::object();
|
|
meta.set("files", Json::object());
|
|
sk::SkillFiles::save_skill_install_meta(home + "/skills", "taste", meta);
|
|
}
|
|
step("uninstall sidecar", "uninstall");
|
|
out("-- tracked-digest removal of an edited file");
|
|
step("install", "install");
|
|
auto meta = sk::SkillFiles::load_skill_install_meta(home + "/skills", "taste");
|
|
std::vector<std::string> keys;
|
|
for (const auto& m : meta->members()) keys.push_back(m.first);
|
|
std::sort(keys.begin(), keys.end());
|
|
out("meta " + j(arr_of(keys)));
|
|
out("meta files " + j(*meta->get("files")));
|
|
{
|
|
Json fixed = Json::object();
|
|
for (const char* k : {"source", "mode", "scope", "upstream", "bundled_version"}) fixed.set(k, *meta->get(k));
|
|
out("meta fixed " + j(fixed));
|
|
}
|
|
chmod((home + "/skills/taste").c_str(), 0500);
|
|
step("install readonly", "install");
|
|
chmod((home + "/skills/taste").c_str(), 0700);
|
|
out("-- error path: skills dir is a file");
|
|
std::string other = root + "/home2";
|
|
PyFs::makedirs(other);
|
|
write(other + "/skills", "file not dir");
|
|
out("blocked => " + bundled.op_skills("install", other));
|
|
out("blocked uninstall => " + bundled.op_skills("uninstall", other));
|
|
sc_skills_install_tool(root);
|
|
}
|
|
|
|
class SkillApp : public sk::SkillHost {
|
|
public:
|
|
std::string home_, cwd_;
|
|
sk::SkillSet skills_;
|
|
int depth_{0};
|
|
std::vector<std::string> approvals;
|
|
int refreshed{0};
|
|
bool approve{true};
|
|
std::function<std::string(const std::string&, int)> runner;
|
|
bool ask_approval(const std::string& what) override { approvals.push_back(what); return approve; }
|
|
std::string home() const override { return home_; }
|
|
std::string cwd() const override { return cwd_; }
|
|
int depth() const override { return depth_; }
|
|
const sk::SkillSet& skills() const override { return skills_; }
|
|
void refresh_skills() override { ++refreshed; skills_ = sk::SkillFiles::discover_skills(home_, cwd_); }
|
|
void audit_skill_install(const std::string&, const std::string&) override {}
|
|
std::string run_worker(const std::string& prompt, int timeout, int) override { return runner(prompt, timeout); }
|
|
};
|
|
|
|
static std::string dispatch(sk::SkillTool& tool, const std::string& name, const Json& args) {
|
|
try {
|
|
if (name == "install_skill") return tool.run_install_skill(args);
|
|
if (name == "load_skill") return tool.run_load_skill(args);
|
|
if (name == "create_skill") return tool.run_create_skill(args);
|
|
} catch (const std::exception& e) {
|
|
return "error: " + PyStr::short_error(e.what());
|
|
}
|
|
return "error: unknown tool " + name;
|
|
}
|
|
|
|
static Json obj(std::initializer_list<std::pair<const char*, Json>> kv) {
|
|
Json o = Json::object();
|
|
for (const auto& p : kv) o.set(p.first, p.second);
|
|
return o;
|
|
}
|
|
|
|
static std::string replace_root(std::string s, const std::string& root) { return PyStr::replace_all(s, root, "<ROOT>"); }
|
|
|
|
static void sc_skills_install_tool(const std::string& root) {
|
|
std::string home = tai_home();
|
|
std::string proj = root + "/proj";
|
|
if (chdir(proj.c_str()) != 0) return;
|
|
SkillApp app;
|
|
app.home_ = home;
|
|
app.cwd_ = proj;
|
|
app.skills_ = sk::SkillFiles::discover_skills(home, proj);
|
|
sk::SkillTool tool(app);
|
|
out("== install_skill / load_skill / create_skill");
|
|
std::string src = root + "/srcskill";
|
|
write(src + "/SKILL.md", "---\nname: from-src\ndescription: installed from a dir\n---\nbody src\n");
|
|
write(src + "/scripts/x.sh", "echo x\n");
|
|
write(src + "/references/r.md", "r\n");
|
|
std::string nested = root + "/nested";
|
|
write(nested + "/inner/SKILL.md", "---\nname: inner\ndescription: nested one\n---\nbody inner\n");
|
|
std::string two = root + "/two";
|
|
write(two + "/a/SKILL.md", "---\nname: aa\ndescription: d\n---\nb\n");
|
|
write(two + "/b/SKILL.md", "---\nname: bb\ndescription: d\n---\nb\n");
|
|
std::string barefile = root + "/bare.md";
|
|
write(barefile, "---\nname: bare-one\ndescription: bare md file\n---\nbody bare\n");
|
|
std::string badfile = root + "/bad.md";
|
|
write(badfile, "no front\n");
|
|
std::string ext = proj + "/.claude/skills/extskill";
|
|
write(ext + "/SKILL.md", "---\nname: extskill\ndescription: external one\n---\nbody ext\n");
|
|
|
|
auto call = [&](const std::string& label, const Json& args) { out(label + " => " + js(dispatch(tool, "install_skill", args))); };
|
|
call("empty", obj({{"source", ""}}));
|
|
call("bad scope", obj({{"source", src}, {"scope", "mars"}}));
|
|
call("bad mode", obj({{"source", src}, {"mode", "link"}}));
|
|
call("missing", obj({{"source", root + "/nope"}}));
|
|
call("dir home", obj({{"source", src}, {"scope", "home"}}));
|
|
call("dir home again denied?", obj({{"source", src}, {"scope", "home"}}));
|
|
app.approve = false;
|
|
call("dir home denied", obj({{"source", src}, {"scope", "home"}}));
|
|
app.approve = true;
|
|
call("dir home force", obj({{"source", src}, {"scope", "home"}, {"force", true}}));
|
|
call("dir project", obj({{"source", src}, {"scope", "project"}, {"name", "renamed"}}));
|
|
call("bad name", obj({{"source", src}, {"name", "Bad Name"}}));
|
|
call("nested", obj({{"source", nested}, {"scope", "home"}}));
|
|
call("two children", obj({{"source", two}, {"scope", "home"}}));
|
|
call("bare file", obj({{"source", barefile}, {"scope", "home"}}));
|
|
call("bad file", obj({{"source", badfile}}));
|
|
call("symlink ext default", obj({{"source", ext}, {"scope", "home"}}));
|
|
call("symlink explicit", obj({{"source", src}, {"scope", "home"}, {"mode", "symlink"}, {"name", "linked"}, {"force", true}}));
|
|
call("symlink bare", obj({{"source", barefile}, {"mode", "symlink"}}));
|
|
call("not a skill source", obj({{"source", "/dev/null"}}));
|
|
out("approvals " + j(arr_of(app.approvals)));
|
|
out("refreshed " + std::to_string(app.refreshed));
|
|
out("skills " + j(arr_of(app.skills_.sorted_names())));
|
|
out("== load_skill");
|
|
app.skills_ = sk::SkillFiles::discover_skills(home, proj);
|
|
for (const std::string name : {"from-src", "deep-research", "extskill", "nonexistent", ""}) {
|
|
out("load " + js(name) + " => " + js(dispatch(tool, "load_skill", obj({{"name", name}}))));
|
|
}
|
|
out("== create_skill");
|
|
app.runner = [](const std::string& prompt, int timeout) { return "worker said: " + PyStr::prefix(prompt, 40) + " t=" + std::to_string(timeout); };
|
|
auto create = [&](const Json& args) { out("create " + j(args) + " => " + js(dispatch(tool, "create_skill", args))); };
|
|
create(obj({{"name", ""}, {"brief", "b"}}));
|
|
create(obj({{"name", "ok-name"}, {"brief", ""}}));
|
|
create(obj({{"name", "ok-name"}, {"brief", "b"}, {"scope", "mars"}}));
|
|
create(obj({{"name", "made"}, {"brief", "make it"}, {"scope", "home"}, {"timeout", 10}}));
|
|
create(obj({{"name", "made2"}, {"brief", "make it"}, {"timeout", "abc"}}));
|
|
create(obj({{"name", "made3"}, {"brief", "x"}, {"timeout", 99999}}));
|
|
app.depth_ = 5;
|
|
out("create depth => " + js(dispatch(tool, "create_skill", obj({{"name", "dd"}, {"brief", "b"}}))));
|
|
app.depth_ = 0;
|
|
app.runner = [&](const std::string& prompt, int) {
|
|
std::string marker = "Create a new agent skill named 'wrote' in ";
|
|
std::string path = prompt.substr(marker.size());
|
|
path = path.substr(0, path.find(".\n"));
|
|
write(path + "/SKILL.md", "---\nname: wrote\ndescription: written by worker\n---\nbody wrote\n");
|
|
return replace_root("done writing " + path, root);
|
|
};
|
|
out("create written => " + js(replace_root(dispatch(tool, "create_skill", obj({{"name", "wrote"}, {"brief", "w"}, {"scope", "home"}})), root)));
|
|
out("create replaced => " + js(replace_root(dispatch(tool, "create_skill", obj({{"name", "wrote"}, {"brief", "w"}, {"scope", "home"}})), root)));
|
|
out("refreshed " + std::to_string(app.refreshed));
|
|
}
|
|
|
|
// ------------------------------------------------------------------ install scenarios
|
|
namespace ins = detz::install;
|
|
|
|
class FakeBox : public ins::ContainerPort {
|
|
public:
|
|
std::optional<std::string> engine;
|
|
std::string state = "running", venv = "venv ready", ensure_error;
|
|
std::optional<std::string> container_engine() override { return engine; }
|
|
std::string box_state(const std::string&) override { return state; }
|
|
std::string box_venv_state(const std::string&) override { return venv; }
|
|
std::string ensure_box() override {
|
|
if (!ensure_error.empty()) throw detz::misc::BackendError(ensure_error);
|
|
return engine ? *engine : std::string();
|
|
}
|
|
};
|
|
|
|
static ins::InstallContext install_ctx() {
|
|
ins::InstallContext ctx;
|
|
const char* py = std::getenv("MISC_TAI_PY");
|
|
ctx.script_path = py ? py : "tai.py";
|
|
return ctx;
|
|
}
|
|
|
|
static void sc_install_hooks(const std::string& root) {
|
|
FakeBox box;
|
|
ins::StdIo io;
|
|
ins::Installer inst(install_ctx(), box, io);
|
|
std::string home = tai_home();
|
|
std::string uh = PyFs::home();
|
|
std::string rc = uh + "/.bashrc", zrc = uh + "/.zshrc";
|
|
write(rc, "# my bashrc\nexport A=1");
|
|
write(zrc, "");
|
|
auto status = [&] { return PyStr::join(inst.status_lines(home), "\n"); };
|
|
std::vector<std::pair<std::string, std::function<std::string()>>> steps = {
|
|
{"bash install", [&] { return inst.op_hook("install", home); }},
|
|
{"bash install again", [&] { return inst.op_hook("install", home); }},
|
|
{"bash upgrade", [&] { return inst.op_hook("upgrade", home); }},
|
|
{"plexer install", [&] { return inst.op_plexer_hook("install", home); }},
|
|
{"plexer again", [&] { return inst.op_plexer_hook("install", home); }},
|
|
{"zsh install", [&] { return inst.op_zsh_hook("install", home); }},
|
|
{"zsh again", [&] { return inst.op_zsh_hook("install", home); }},
|
|
{"fish install", [&] { return inst.op_fish_hook("install", home); }},
|
|
{"zsh plexer install", [&] { return inst.op_zsh_plexer_hook("install", home); }},
|
|
{"fish plexer install", [&] { return inst.op_fish_plexer_hook("install", home); }},
|
|
{"status", status},
|
|
{"bash reinstall", [&] { return inst.op_hook("reinstall", home); }},
|
|
{"plexer reinstall", [&] { return inst.op_plexer_hook("reinstall", home); }},
|
|
{"zsh uninstall", [&] { return inst.op_zsh_hook("uninstall", home); }},
|
|
{"fish uninstall", [&] { return inst.op_fish_hook("uninstall", home); }},
|
|
{"zsh uninstall again", [&] { return inst.op_zsh_hook("uninstall", home); }},
|
|
{"bash uninstall", [&] { return inst.op_hook("uninstall", home); }},
|
|
{"plexer uninstall", [&] { return inst.op_plexer_hook("uninstall", home); }},
|
|
{"zsh plexer uninstall", [&] { return inst.op_zsh_plexer_hook("uninstall", home); }},
|
|
{"fish plexer uninstall", [&] { return inst.op_fish_plexer_hook("uninstall", home); }},
|
|
{"status after", status},
|
|
};
|
|
for (const auto& st : steps) out(st.first + " => " + js(st.second()));
|
|
out("rc " + js(read(rc)));
|
|
out("zrc " + js(read(zrc)));
|
|
out("== stale and duplicated blocks");
|
|
const std::string& begin = ins::InstallConsts::BASHRC_MARK_BEGIN();
|
|
const std::string& end = ins::InstallConsts::BASHRC_MARK_END();
|
|
write(rc, "head\n" + begin + "\nstale content\n" + end + "\nmid\n" + begin + "\nsecond stale\n" + end + "\ntail\n");
|
|
out("install over stale => " + js(inst.op_hook("install", home)));
|
|
out("rc " + js(read(rc)));
|
|
write(rc, "x\n" + begin + "\nunterminated\n");
|
|
out("install over unterminated => " + js(inst.op_hook("install", home)));
|
|
out("rc " + js(read(rc)));
|
|
out("uninstall unterminated => " + js(inst.op_hook("uninstall", home)));
|
|
out("rc " + js(read(rc)));
|
|
write(rc, "crlf\r\nline\r\n");
|
|
out("install crlf => " + js(inst.op_hook("install", home)));
|
|
out("rc " + js(read(rc)));
|
|
chmod(rc.c_str(), 0400);
|
|
out("readonly install => " + js(inst.op_plexer_hook("install", home)));
|
|
chmod(rc.c_str(), 0600);
|
|
auto present = [&](const std::string& path) {
|
|
Json a = Json::array();
|
|
for (bool b : {ins::Installer::hook_present(path), ins::Installer::zsh_hook_present(path), ins::Installer::fish_hook_present(path),
|
|
ins::Installer::plexer_hook_present(path), ins::Installer::zsh_plexer_hook_present(path), ins::Installer::fish_plexer_hook_present(path)})
|
|
a.push(b);
|
|
return a;
|
|
};
|
|
out("present " + j(present(rc)));
|
|
out("present missing " + j(present(root + "/nope")));
|
|
for (const std::string shell : {"bash", "zsh", "fish", "other"}) out("rc path " + shell + " " + ins::Install::shell_rc_path(shell));
|
|
setenv("XDG_CONFIG_HOME", (root + "/xdg").c_str(), 1);
|
|
out("rc path fish xdg " + ins::Install::shell_rc_path("fish"));
|
|
unsetenv("XDG_CONFIG_HOME");
|
|
Json sp = Json::array();
|
|
for (const char* n : {"zsh", "fish", "bash", "nosuchshell"}) sp.push(ins::Install::shell_present(n));
|
|
out("shell_present " + j(sp));
|
|
}
|
|
|
|
class InstallApp : public sk::SkillHost {
|
|
public:
|
|
std::string home_, cwd_;
|
|
sk::SkillSet skills_;
|
|
std::vector<std::string> approvals;
|
|
bool approve{true};
|
|
bool ask_approval(const std::string& what) override { approvals.push_back(what); return approve; }
|
|
std::string home() const override { return home_; }
|
|
std::string cwd() const override { return cwd_; }
|
|
const sk::SkillSet& skills() const override { return skills_; }
|
|
void refresh_skills() override { skills_ = sk::SkillFiles::discover_skills(home_, cwd_); }
|
|
void audit_skill_install(const std::string&, const std::string&) override {}
|
|
std::string run_worker(const std::string&, int, int) override { return ""; }
|
|
};
|
|
|
|
static void set_box(FakeBox& box, std::optional<std::string> engine, const std::string& state = "running", const std::string& venv = "venv ready", const std::string& err = "") {
|
|
box.engine = engine;
|
|
box.state = state;
|
|
box.venv = venv;
|
|
box.ensure_error = err;
|
|
}
|
|
|
|
static void sc_install_ops(const std::string& root) {
|
|
FakeBox box;
|
|
set_box(box, std::string("podman"));
|
|
ins::StdIo io;
|
|
ins::Installer inst(install_ctx(), box, io);
|
|
std::string home = tai_home();
|
|
std::string uh = PyFs::home();
|
|
out("-- binary");
|
|
for (const std::string mode : {"install", "install", "upgrade", "reinstall", "uninstall", "uninstall", "install"}) out("binary " + mode + " => " + js(inst.op_binary(mode, home)));
|
|
std::string target = uh + "/.local/bin/tai.py";
|
|
{
|
|
struct stat st;
|
|
stat(target.c_str(), &st);
|
|
struct stat st2;
|
|
stat(install_ctx().script_path.c_str(), &st2);
|
|
char buf[16];
|
|
std::snprintf(buf, sizeof buf, "0o%o", static_cast<unsigned>(st.st_mode & 0777));
|
|
out(std::string("binary mode ") + buf + " size_equal " + py_bool(st.st_size == st2.st_size));
|
|
}
|
|
write(target, "VERSION = \"0.0.1\"\n");
|
|
out("binary refresh => " + js(inst.op_binary("install", home)));
|
|
out("binary refresh upgrade => " + js(inst.op_binary("upgrade", home)));
|
|
auto opt_js = [](const std::optional<std::string>& v) { return v ? js(*v) : std::string("null"); };
|
|
out("script_version " + opt_js(inst.script_version(target)) + " " + opt_js(inst.script_version(root + "/nope")) + " " + opt_js(inst.script_version(install_ctx().script_path)));
|
|
write(root + "/v1", "x\nVERSION = \"1.2.3\"\ny\n");
|
|
write(root + "/v2", "VERSION = \"1.2\"\n VERSION = \"1.2.3\"\nVERSION = \"a.b.c\"\n");
|
|
write(root + "/v3", "VERSION = \"4.5.6\"");
|
|
write(root + "/v4", "VERSION = \"4.5.6\" \n");
|
|
{
|
|
std::string s = "[";
|
|
bool first = true;
|
|
for (const char* n : {"v1", "v2", "v3", "v4"}) {
|
|
s += std::string(first ? "" : ", ") + (inst.script_version(root + "/" + n) ? js(*inst.script_version(root + "/" + n)) : std::string("null"));
|
|
first = false;
|
|
}
|
|
out("script_version v1..v4 " + s + "]");
|
|
}
|
|
out("-- venv");
|
|
for (const std::string mode : {"install", "install", "reinstall", "uninstall", "uninstall", "install"}) out("venv " + mode + " => " + js(inst.op_venv(mode, home)));
|
|
{
|
|
auto vp = ins::Install::venv_python(home);
|
|
out("venv helpers " + js(ins::Install::venv_dir(home)) + " " + js(vp ? *vp : std::string()) + " " + py_bool(ins::Install::venv_available()));
|
|
}
|
|
out("-- services (no systemctl)");
|
|
for (const std::string mode : {"install", "install", "reinstall", "upgrade", "uninstall", "uninstall"}) out("scheduler " + mode + " => " + js(inst.op_scheduler_service(mode, home)));
|
|
out("telegram without token => " + js(inst.op_telegram_service("install", home)));
|
|
out("service_exec " + js(inst.service_exec(home, "/x/tai.py", {"--a", "b"})));
|
|
out("unit_dir " + js(ins::Systemd::unit_dir(home)));
|
|
{
|
|
auto st = ins::Systemd::service_state("tai-scheduler.service");
|
|
Json a = Json::array();
|
|
a.push(st.first).push(st.second);
|
|
out("service_state " + j(a));
|
|
}
|
|
out("activate " + js(ins::Systemd::activate_service("tai-scheduler.service")));
|
|
{
|
|
auto r = ins::Systemd::remove_service_unit(home, "tai-scheduler.service");
|
|
Json a = Json::array();
|
|
a.push(r.first).push(r.second);
|
|
out("remove " + j(a));
|
|
}
|
|
out("-- container");
|
|
for (const std::string mode : {"install", "upgrade", "reinstall", "uninstall"}) out("container " + mode + " => " + js(inst.op_container(mode, home)));
|
|
set_box(box, std::string("podman"), "stopped", "no venv (reinstall container to rebuild)");
|
|
for (const std::string mode : {"install", "reinstall"}) out("container stopped " + mode + " => " + js(inst.op_container(mode, home)));
|
|
set_box(box, std::string("podman"), "running", "venv ready", "engine exploded");
|
|
out("container failing => " + js(inst.op_container("install", home)));
|
|
set_box(box, std::nullopt);
|
|
for (const std::string mode : {"install", "uninstall"}) out("container noengine " + mode + " => " + js(inst.op_container(mode, home)));
|
|
set_box(box, std::string("podman"));
|
|
out("-- install_report");
|
|
struct Rep { std::string action; std::vector<std::string> targets; };
|
|
std::vector<Rep> reps = {{"install", {"skills", "bash-hook", "venv"}}, {"status", {"skills"}}, {"upgrade", ins::InstallConsts::INSTALL_TARGETS()},
|
|
{"uninstall", {"skills", "bash-hook", "plexer-hook", "binary"}}};
|
|
for (const auto& r : reps) out("report " + r.action + " " + PyStr::replace_all(PyStr::replace_all(j(arr_of(r.targets)), "\"", "'"), ",", ",") + " =>\n" + inst.install_report(r.action, r.targets, home));
|
|
out("-- install_self");
|
|
for (const std::string shell : {"/bin/bash", "/usr/bin/zsh", "/usr/bin/fish", "", "/bin/sh"}) {
|
|
setenv("SHELL", shell.c_str(), 1);
|
|
out("shell " + js(shell));
|
|
std::cout.flush();
|
|
inst.install_self();
|
|
}
|
|
out("-- install tool");
|
|
InstallApp app;
|
|
app.home_ = home;
|
|
app.cwd_ = root;
|
|
auto run_tool = [&](const Json& args) {
|
|
return inst.run_install(args, home, nullptr, [&](const std::string& w) { return app.ask_approval(w); }, [&] { app.refresh_skills(); });
|
|
};
|
|
std::vector<Json> tool_args = {
|
|
obj({{"action", "bogus"}}), obj({{"action", "status"}}), obj({{"action", "STATUS "}, {"targets", arr_of({"skills"})}}), obj({{"action", "install"}, {"targets", arr_of({"nope"})}}),
|
|
obj({{"action", "install"}, {"targets", Json::array()}}), obj({{"action", "install"}, {"targets", "skills"}}), obj({{"action", "install"}, {"targets", arr_of({"skills"})}}),
|
|
obj({{"action", "uninstall"}, {"targets", arr_of({"skills", "bash-hook"})}}), Json::object()};
|
|
for (const auto& args : tool_args) {
|
|
app.skills_ = sk::SkillSet();
|
|
std::string res = run_tool(args);
|
|
out("tool " + j(args) + " => " + js(res) + " | skills=" + j(arr_of(app.skills_.sorted_names())));
|
|
}
|
|
app.approve = false;
|
|
out("tool denied => " + js(run_tool(obj({{"action", "install"}, {"targets", arr_of({"skills"})}}))));
|
|
out("approvals " + j(arr_of(app.approvals)));
|
|
}
|
|
|
|
static void sc_install_noengine(const std::string&) {
|
|
FakeBox box;
|
|
set_box(box, std::nullopt);
|
|
ins::StdIo io;
|
|
ins::Installer inst(install_ctx(), box, io);
|
|
std::string home = tai_home();
|
|
auto lines = inst.status_lines(home);
|
|
Json a = Json::array();
|
|
for (std::size_t i = lines.size() - 3; i < lines.size(); ++i) a.push(lines[i]);
|
|
out(j(a));
|
|
for (const std::string mode : {"install", "uninstall"}) out("container " + mode + " => " + js(inst.op_container(mode, home)));
|
|
}
|
|
|
|
static void sc_vault_line(const std::string&) {
|
|
std::string home = tai_home();
|
|
out("empty " + js(ins::Installer::vault_line(home)));
|
|
write(home + "/memory.db", "");
|
|
out("kept " + js(ins::Installer::vault_line(home)));
|
|
}
|
|
|
|
// ------------------------------------------------------------------ scheduler scenario
|
|
namespace sc = detz::sched;
|
|
using detz::misc::DbValue;
|
|
using detz::misc::SqliteDb;
|
|
|
|
static Json db_json(const DbValue& v) {
|
|
if (v.is_null()) return Json();
|
|
if (v.is_int()) return Json(v.as_int());
|
|
return Json(v.as_text());
|
|
}
|
|
static Json row_json(const detz::misc::DbRow& r) {
|
|
Json a = Json::array();
|
|
for (const auto& v : r) a.push(db_json(v));
|
|
return a;
|
|
}
|
|
|
|
// Seal with passphrase disabled: lock is the identity, unlock follows Seal.unlock's error paths.
|
|
class DisabledSeal : public sc::SealPort {
|
|
public:
|
|
bool enabled() const override { return false; }
|
|
std::string lock(const std::string& plain) const override { return plain; }
|
|
std::string unlock(const std::string& sealed) const override {
|
|
if (!is_sealed(sealed)) throw sc::SealError("not sealed");
|
|
std::size_t d1 = sealed.find('$'), d2 = sealed.find('$', d1 + 1), d3 = sealed.find('$', d2 + 1);
|
|
if (d2 == std::string::npos || d3 == std::string::npos) throw sc::SealError("sealed value is corrupt");
|
|
throw sc::SealError("seal check failed");
|
|
}
|
|
std::string safe_unlock(const std::string& sealed) const override {
|
|
try { return unlock(sealed); } catch (const sc::SealError&) { return "[sealed: decrypt failed]"; }
|
|
}
|
|
};
|
|
|
|
// The tag side of Store as the schedule code uses it (no plural folding needed for the test vocabulary)
|
|
class SqlTags : public sc::TagPort {
|
|
public:
|
|
SqlTags(SqliteDb& db) : db_(db) {}
|
|
void tag_item(const std::string& item, const std::vector<std::string>& tags, const std::string& profile) override {
|
|
for (const auto& t : detz::misc::TagNames::normalize_tags(tags)) db_.execute("INSERT OR IGNORE INTO tags (item, tag, profile) VALUES (?, ?, ?)", {item, t, profile});
|
|
}
|
|
void set_item_tags(const std::string& item, const std::vector<std::string>& tags, const std::string& profile) override {
|
|
db_.execute("DELETE FROM tags WHERE item = ? AND profile = ?", {item, profile});
|
|
tag_item(item, tags, profile);
|
|
}
|
|
std::vector<std::string> item_tags(const std::string& item, const std::string& profile) override {
|
|
std::vector<std::string> out_tags;
|
|
for (const auto& r : db_.query("SELECT tag FROM tags WHERE item = ? AND profile = ? ORDER BY tag", {item, profile})) out_tags.push_back(r[0].as_text());
|
|
return out_tags;
|
|
}
|
|
private:
|
|
SqliteDb& db_;
|
|
};
|
|
|
|
// Registry with spawn_agent/poll_agent/running_agents semantics of tai.py and the Gate runner of the Python twin.
|
|
class FakeAgents : public sc::AgentPort {
|
|
public:
|
|
struct Rec {
|
|
std::string task, status = "running", result;
|
|
std::thread th;
|
|
};
|
|
std::mutex m;
|
|
std::map<int, std::unique_ptr<Rec>> recs;
|
|
int next = 1;
|
|
bool report_zero = false;
|
|
std::mutex gm;
|
|
std::condition_variable gcv;
|
|
bool released = false;
|
|
|
|
~FakeAgents() override {
|
|
release();
|
|
for (auto& kv : recs) {
|
|
if (kv.second->th.joinable()) kv.second->th.join();
|
|
}
|
|
}
|
|
void release() {
|
|
std::lock_guard<std::mutex> lock(gm);
|
|
released = true;
|
|
gcv.notify_all();
|
|
}
|
|
int running_agents() override {
|
|
if (report_zero) return 0;
|
|
std::lock_guard<std::mutex> lock(m);
|
|
int n = 0;
|
|
for (auto& kv : recs) n += kv.second->status == "running";
|
|
return n;
|
|
}
|
|
int spawn_agent(const std::string& task, const std::string&, int64_t) override {
|
|
std::lock_guard<std::mutex> lock(m);
|
|
int live = 0;
|
|
for (auto& kv : recs) live += kv.second->status == "running";
|
|
if (live >= 8) throw sc::AgentsFull("too many live agents (" + std::to_string(live) + " of 8)");
|
|
int id = next++;
|
|
auto rec = std::make_unique<Rec>();
|
|
rec->task = task;
|
|
Rec* raw = rec.get();
|
|
recs[id] = std::move(rec);
|
|
raw->th = std::thread([this, raw, task] {
|
|
std::string result, status;
|
|
try {
|
|
if (task.rfind("slow:", 0) == 0) {
|
|
std::unique_lock<std::mutex> lock(gm);
|
|
gcv.wait_for(lock, std::chrono::seconds(30), [this] { return released; });
|
|
result = "slow done " + task;
|
|
} else if (task.find("boom") != std::string::npos) {
|
|
throw std::runtime_error("boom");
|
|
} else {
|
|
result = "ok:" + task;
|
|
}
|
|
status = result.find("[time limit reached]") != std::string::npos ? "timeout" : "done";
|
|
} catch (const std::exception& e) {
|
|
result = std::string("worker failed: ") + PyStr::short_error(e.what());
|
|
status = "error";
|
|
}
|
|
std::lock_guard<std::mutex> lock(m);
|
|
raw->result = result;
|
|
raw->status = status;
|
|
});
|
|
return id;
|
|
}
|
|
std::pair<std::string, std::string> poll_agent(int id, double wait) override {
|
|
Rec* rec;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m);
|
|
auto it = recs.find(id);
|
|
if (it == recs.end()) return {"missing", "no agent " + std::to_string(id)};
|
|
rec = it->second.get();
|
|
}
|
|
auto deadline = std::chrono::steady_clock::now() + std::chrono::duration<double>(wait);
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(m);
|
|
if (rec->status != "running") break;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
}
|
|
std::lock_guard<std::mutex> lock(m);
|
|
if (rec->status == "running") return {rec->status, "agent " + std::to_string(id) + " still running (0s elapsed)"};
|
|
if (rec->status == "error") return {rec->status, rec->result};
|
|
std::string text = rec->result;
|
|
std::size_t n = PyStr::len(text);
|
|
if (n > 6000 + 2000 + 50) text = PyStr::prefix(text, 6000) + "\n...[" + std::to_string(n - 6000 - 2000) + " chars truncated]...\n" + PyStr::suffix(text, 2000);
|
|
if (text.empty()) text = "(empty result)";
|
|
return {rec->status, text};
|
|
}
|
|
};
|
|
|
|
class SchedApp : public sc::ScheduleHost {
|
|
public:
|
|
std::vector<std::string> approvals;
|
|
bool approve = true;
|
|
sc::ScheduleVault* vault_ = nullptr;
|
|
bool ask_approval(const std::string& what) override { approvals.push_back(what); return approve; }
|
|
std::string active_profile() const override { return "t"; }
|
|
sc::ScheduleVault& vault() override { return *vault_; }
|
|
};
|
|
|
|
static void sched_rows(SqliteDb& db, const std::string& label) {
|
|
out("-- " + label);
|
|
for (const auto& r : db.query("SELECT id, name, prompt, profile, every_sec, next_run, timeout, status, last_status, last_result, updated FROM schedules ORDER BY id")) out("row " + j(row_json(r)));
|
|
Json tags = Json::array();
|
|
for (const auto& r : db.query("SELECT item, tag, profile FROM tags WHERE item LIKE 'sched:%' ORDER BY item, tag")) tags.push(row_json(r));
|
|
out("tags " + j(tags));
|
|
}
|
|
|
|
static void sc_sched(const std::string&) {
|
|
std::string db_path = tai_home() + "/memory.db";
|
|
SqliteDb db(db_path, 30, true);
|
|
DisabledSeal seal;
|
|
SqlTags tags(db);
|
|
FakeAgents agents;
|
|
sc::Scheduler scheduler(db_path, seal, agents);
|
|
sc::ScheduleVault vault(db, seal, tags, "t");
|
|
auto quiesce = [&] { scheduler.join_watchers(); };
|
|
auto item_json = [](const sc::ScheduleItem& it) {
|
|
Json o = Json::object();
|
|
o.set("id", it.id).set("name", it.name).set("prompt", it.prompt).set("profile", it.profile).set("every", it.every).set("next_run", it.next_run).set("timeout", it.timeout);
|
|
o.set("status", it.status).set("last", it.last).set("result", it.result).set("created", it.created).set("tags", arr_of(it.tags));
|
|
return o;
|
|
};
|
|
const std::string NOW = "2026-10-25T12:00:00+00:00";
|
|
out("== list and format");
|
|
auto items = vault.list_schedules();
|
|
for (const auto& it : items) out("item " + j(item_json(it)));
|
|
out("format:\n" + sc::Scheduler::format_schedule_lines(items, detz::misc::DateTime::fromisoformat(NOW)));
|
|
out("format empty: " + sc::Scheduler::format_schedule_lines({}, detz::misc::DateTime::fromisoformat(NOW)));
|
|
{
|
|
Json a = Json::array();
|
|
for (const auto& it : vault.list_schedules("other")) a.push(item_json(it));
|
|
out("other profile: " + j(a));
|
|
}
|
|
out("== running leases");
|
|
std::string nowiso = detz::misc::DateTime::now_utc().isoformat();
|
|
struct Lease { std::string status, updated; int64_t timeout; };
|
|
for (const auto& l : std::vector<Lease>{{"running", nowiso, 600}, {"running", "2000-01-01T00:00:00+00:00", 600}, {"done", "", 0}, {"running", "garbage", 10}, {"running", nowiso, 0}}) {
|
|
out("running " + l.status + " " + (l.updated.rfind("2026", 0) == 0 ? std::string("now") : l.updated) + " " + std::to_string(l.timeout) + " => " +
|
|
py_bool(sc::Scheduler::schedule_running(l.status, l.updated, l.timeout)));
|
|
}
|
|
out("== sealed rows (added now: Store refuses to open with them present and no passphrase)");
|
|
vault.add_schedule("sealed", "tai1$AAAA$BBBB$CCCC", "t", 0, "2026-05-01T00:00:00+00:00", 300);
|
|
vault.add_schedule("sealed-corrupt", "tai1$bad", "t", 0, "2026-05-02T00:00:00+00:00", 300);
|
|
out("== tick 1");
|
|
std::cout.flush();
|
|
int fired = scheduler.scheduler_tick();
|
|
out("fired " + std::to_string(fired));
|
|
quiesce();
|
|
sched_rows(db, "after tick 1");
|
|
out("== tick 2");
|
|
out("fired " + std::to_string(scheduler.scheduler_tick()));
|
|
quiesce();
|
|
out("== cap phase");
|
|
for (int k = 0; k < 9; ++k) vault.add_schedule("slow" + std::to_string(k), "slow:" + std::to_string(k), "t", 315360000, "2025-01-0" + std::to_string(k + 1) + "T00:00:00+00:00", 600);
|
|
out("fired " + std::to_string(scheduler.scheduler_tick()));
|
|
out("fired " + std::to_string(scheduler.scheduler_tick()));
|
|
out("running agents " + std::to_string(agents.running_agents()));
|
|
out("fired " + std::to_string(scheduler.scheduler_tick()));
|
|
sched_rows(db, "after cap ticks");
|
|
out("== AgentsFull restore branch");
|
|
vault.add_schedule("full-once", "slow:once", "t", 0, "2025-02-01T00:00:00+00:00", 600);
|
|
agents.report_zero = true;
|
|
out("fired " + std::to_string(scheduler.scheduler_tick()));
|
|
agents.report_zero = false;
|
|
sched_rows(db, "after AgentsFull");
|
|
agents.release();
|
|
quiesce();
|
|
sched_rows(db, "after release");
|
|
out("== schedule tool");
|
|
SchedApp app;
|
|
app.vault_ = &vault;
|
|
sc::ScheduleTool tool(app);
|
|
auto dispatch_tool = [&](const std::string& name, const Json& args) {
|
|
try {
|
|
if (name == "schedule") return tool.run_schedule(args);
|
|
if (name == "unschedule") return tool.run_unschedule(args);
|
|
return tool.run_schedules(args);
|
|
} catch (const std::exception& e) {
|
|
return "error: " + PyStr::short_error(e.what());
|
|
}
|
|
};
|
|
std::vector<Json> calls = {
|
|
obj({{"prompt", ""}}), obj({{"prompt", "p"}}), obj({{"prompt", "p"}, {"at", "2099-01-01T10:00"}, {"every", 120}}), obj({{"prompt", "p"}, {"at", "nonsense"}}),
|
|
obj({{"prompt", "p"}, {"every", "abc"}}), obj({{"prompt", "p"}, {"every", 30}}),
|
|
obj({{"prompt", "tool one"}, {"at", "2099-01-01T10:00"}, {"name", std::string(100, 'N')}, {"timeout", 5}, {"tags", arr_of({"Alpha", "beta!", "", std::string(40, 'x')})}}),
|
|
obj({{"prompt", "tool two"}, {"every", 3600}, {"timeout", "abc"}, {"profile", "t"}}), obj({{"prompt", "tool three"}, {"every", "7200"}, {"timeout", 99999}}),
|
|
obj({{"prompt", "p"}, {"every", 60}, {"profile", "other"}}), obj({{"prompt", "p"}, {"every", 60}, {"profile", "bad profile!"}}),
|
|
obj({{"prompt", "p"}, {"every", 60}, {"tags", "notalist"}}), obj({{"prompt", "p"}, {"every", 60}, {"tags", [] { Json a = Json::array(); a.push(1).push(2); return a; }()}}),
|
|
obj({{"prompt", "tool four"}, {"every", 86400}, {"tags", Json::array()}}), obj({{"prompt", " padded "}, {"at", "2099-02-02T02:02:02Z"}, {"timeout", 0}}),
|
|
obj({{"prompt", "p"}, {"at", "9999-12-31T24:00"}}), obj({{"prompt", "p"}, {"at", "0001-01-01T00:00"}}), obj({{"prompt", "p"}, {"every", 60.5}}), obj({{"prompt", "p"}, {"every", true}}),
|
|
};
|
|
for (const auto& args : calls) out("schedule " + j(args) + " => " + js(dispatch_tool("schedule", args)));
|
|
app.approve = false;
|
|
out("schedule denied => " + js(dispatch_tool("schedule", obj({{"prompt", "denied"}, {"every", 60}}))));
|
|
app.approve = true;
|
|
out("schedules => " + js(dispatch_tool("schedules", Json::object())));
|
|
for (const auto& args : std::vector<Json>{obj({{"id", 999}}), obj({{"id", "abc"}}), obj({{"id", 0}}), Json::object(), obj({{"id", "13"}}), obj({{"id", 14.0}})}) {
|
|
out("unschedule " + j(args) + " => " + js(dispatch_tool("unschedule", args)));
|
|
}
|
|
app.approve = false;
|
|
out("unschedule denied => " + js(dispatch_tool("unschedule", obj({{"id", 15}}))));
|
|
app.approve = true;
|
|
out("approvals " + j(arr_of(app.approvals)));
|
|
sched_rows(db, "final");
|
|
out("remove nonexistent " + py_bool(vault.remove_schedule(99999)));
|
|
}
|
|
|
|
// ------------------------------------------------------------------ rag scenario
|
|
namespace rg = detz::rag;
|
|
|
|
static Json hit_json(const rg::FtsHit& h) {
|
|
Json o = Json::object();
|
|
o.set("item", db_json(h.item)).set("kind", h.kind).set("sub", h.sub).set("title", h.title).set("snippet", h.snippet).set("rank", h.rank);
|
|
return o;
|
|
}
|
|
static Json ids_json(const std::vector<DbValue>& v) {
|
|
Json a = Json::array();
|
|
for (const auto& x : v) a.push(db_json(x));
|
|
return a;
|
|
}
|
|
static Json mean_json(const rg::EvalMean& m) {
|
|
Json o = Json::object();
|
|
o.set("hit", m.hit).set("mrr", m.mrr).set("ndcg", m.ndcg).set("cp", m.cp);
|
|
return o;
|
|
}
|
|
static Json result_json(const rg::EvalResult& r) {
|
|
Json o = Json::object();
|
|
o.set("k", r.k).set("goldens", r.goldens).set("overall", mean_json(r.overall));
|
|
Json cats = Json::object();
|
|
for (const auto& c : r.by_category) {
|
|
Json e = Json::object();
|
|
e.set("goldens", c.second.goldens).set("mean", mean_json(c.second.mean));
|
|
cats.set(c.first, e);
|
|
}
|
|
o.set("by_category", cats);
|
|
Json misses = Json::array();
|
|
for (const auto& m : r.misses) {
|
|
Json e = Json::object();
|
|
e.set("query", m.query).set("category", m.category);
|
|
misses.push(e);
|
|
}
|
|
o.set("misses", misses);
|
|
return o;
|
|
}
|
|
static Json pairs_json(const rg::ImplicitPairs& p) {
|
|
Json o = Json::object();
|
|
o.set("searches", p.searches).set("read_follow", p.read_follow);
|
|
Json pairs = Json::array();
|
|
for (const auto& x : p.pairs) {
|
|
Json e = Json::object();
|
|
e.set("query", x.query).set("read", x.read).set("rank", x.rank ? Json(*x.rank) : Json()).set("shown", x.shown);
|
|
pairs.push(e);
|
|
}
|
|
o.set("pairs", pairs).set("zero_hit", arr_of(p.zero_hit)).set("zero_hit_count", p.zero_hit_count);
|
|
return o;
|
|
}
|
|
|
|
class RagApp : public rg::RagHost {
|
|
public:
|
|
RagApp(SqliteDb& db, rg::FtsSearch& fts, rg::ChunkStore& chunks) : db_(db), fts_(fts), chunks_(chunks) {}
|
|
std::string active_profile() const override { return "t"; }
|
|
bool sealed() const override { return false; }
|
|
std::vector<rg::FtsHit> fts_search(const std::string& q, const std::vector<std::string>& kinds, const std::string& profile, int limit) override { return fts_.fts_search(q, kinds, profile, limit); }
|
|
std::vector<rg::EventRow> events_like(const std::string&, const std::string&, int) override { return {}; }
|
|
void log_event(const std::string& profile, const std::string& role, const std::string& kind, const std::string& text) override {
|
|
std::string value = PyStr::prefix(text, 2000);
|
|
db_.execute("INSERT INTO events (profile, ts, role, kind, text, tags) VALUES (?, ?, ?, ?, ?, ?)", {profile, detz::sched::ScheduleTime::now_iso(), role, kind, value, " " + role + " " + kind + " "});
|
|
}
|
|
std::map<int64_t, rg::ChunkRow> get_chunks_by_ids(const std::vector<int64_t>& ids, const std::string& profile) override { return chunks_.get_chunks_by_ids(ids, profile); }
|
|
std::vector<rg::GraphEdge> edges_for(const std::string& item, const std::string& profile) override {
|
|
std::vector<rg::GraphEdge> out_edges;
|
|
for (const auto& r : db_.query("SELECT src, dst, relation FROM edges WHERE profile = ? AND (src = ? OR dst = ?)", {profile, item, item})) {
|
|
out_edges.push_back({r[0].as_text() == item ? r[1].as_text() : r[0].as_text(), r[2].as_text()});
|
|
}
|
|
return out_edges;
|
|
}
|
|
std::string node_title(const std::string& node, const std::string& profile) override {
|
|
std::size_t colon = node.find(':');
|
|
if (colon == std::string::npos) return node;
|
|
std::string kind = node.substr(0, colon), key = node.substr(colon + 1);
|
|
if (kind == "mem") {
|
|
auto rows = db_.query("SELECT title FROM records WHERE profile = ? AND id = ?", {profile, node});
|
|
if (rows.empty()) return node;
|
|
std::string t = rows[0][0].as_text();
|
|
return t.empty() ? node : t;
|
|
}
|
|
if (kind == "secret") return "secret:" + key;
|
|
auto rows = db_.query("SELECT name, prompt FROM schedules WHERE profile = ? AND id = ?", {profile, std::stoll(key)});
|
|
if (rows.empty()) return node;
|
|
std::string name = rows[0][0].as_text();
|
|
return name.empty() ? "schedule #" + key : name;
|
|
}
|
|
private:
|
|
SqliteDb& db_;
|
|
rg::FtsSearch& fts_;
|
|
rg::ChunkStore& chunks_;
|
|
};
|
|
|
|
static void sc_rag(const std::string&) {
|
|
SqliteDb db(tai_home() + "/memory.db", 30, true);
|
|
rg::FtsSearch fts(db, "t");
|
|
rg::ChunkStore chunks(db, "t");
|
|
out("== fts_search");
|
|
std::vector<std::string> queries = {"alpha", "alpha bravo", "zebra", "caf\xc3\xa9", "r\xc3\xa9sum\xc3\xa9 na\xc3\xafve", "snake_case", "numbers123", "nothingmatchesthis", "", "!!!", "retrieval index", "wh",
|
|
"ALPHA", std::string(50, 'x'), "unique zebra jumps", "message", "file", "ro", "v2beta kernel"};
|
|
std::vector<std::vector<std::string>> kind_sets = {{}, {"record"}, {"chunk"}, {"audit"}, {"event"}, {"record", "chunk"}};
|
|
for (const auto& q : queries) {
|
|
for (const auto& kinds : kind_sets) {
|
|
for (int limit : {10, 3}) {
|
|
Json a = Json::array();
|
|
for (const auto& h : fts.fts_search(q, kinds, "t", limit)) a.push(hit_json(h));
|
|
out("search " + js(q) + " " + j(arr_of(kinds)) + " " + std::to_string(limit) + " => " + j(a));
|
|
}
|
|
}
|
|
}
|
|
{
|
|
Json a = Json::array();
|
|
for (const auto& h : fts.fts_search("alpha", {"record"}, "other", 10)) a.push(hit_json(h));
|
|
out("other profile " + j(a));
|
|
Json l = Json::array();
|
|
for (int n : {0, -5, 1, 100, 500}) l.push(static_cast<int64_t>(fts.fts_search("alpha", {}, "t", n).size()));
|
|
out("limits " + j(l));
|
|
}
|
|
out("== eval");
|
|
out("tokens " + j(arr_of(rg::RetrievalEval::tokens("The quick Brown fox jumped_over 123456 abcdefg ABCDEFG caf\xc3\xa9s r\xc3\xa9sum\xc3\xa9s"))));
|
|
out("tokens4 " + j(arr_of(rg::RetrievalEval::tokens("The quick Brown fox", 4))));
|
|
auto goldens = rg::RetrievalEval::goldens_from_vault(db, "t");
|
|
out("goldens " + std::to_string(goldens.size()));
|
|
for (const auto& g : goldens) {
|
|
Json o = Json::object();
|
|
o.set("query", g.query).set("expected", ids_json(g.expected)).set("category", g.category).set("source", db_json(g.source));
|
|
out("golden " + j(o));
|
|
}
|
|
auto result = rg::RetrievalEval::score(fts, goldens, "t");
|
|
out("score " + j(result_json(result)));
|
|
auto pairs = rg::RetrievalEval::implicit_pairs(db, "t", false);
|
|
out("pairs " + j(pairs_json(pairs)));
|
|
out("report:\n" + rg::RetrievalEval::format_report(result, pairs));
|
|
out("empty report:\n" + rg::RetrievalEval::format_report(rg::RetrievalEval::score(fts, {}, "t"), rg::RetrievalEval::implicit_pairs(db, "nobody", false)));
|
|
out("small k " + j(result_json(rg::RetrievalEval::score(fts, std::vector<rg::Golden>(goldens.begin(), goldens.begin() + 12), "t", {"record"}, 1, 2))));
|
|
{
|
|
auto ro = [](std::optional<int> r) { return r ? Json(*r) : Json(); };
|
|
out("rank_of " + j(ro(rg::RetrievalEval::rank_of(DbValue("b"), {DbValue("a"), DbValue("b")}))) + " " + j(ro(rg::RetrievalEval::rank_of(DbValue("z"), {DbValue("a")}))) + " " +
|
|
j(ro(rg::RetrievalEval::best_rank({DbValue("z"), DbValue("a")}, {DbValue("q"), DbValue("a"), DbValue("z")}))));
|
|
Json m = Json::array();
|
|
m.push(rg::RetrievalEval::hit_at_k(3)).push(rg::RetrievalEval::hit_at_k(6)).push(rg::RetrievalEval::hit_at_k(std::nullopt)).push(rg::RetrievalEval::reciprocal_rank(4));
|
|
m.push(rg::RetrievalEval::ndcg_at_k(3)).push(rg::RetrievalEval::ndcg_at_k(9));
|
|
m.push(rg::RetrievalEval::context_precision({DbValue("a"), DbValue("c")}, {DbValue("a"), DbValue("b"), DbValue("c"), DbValue("d")}));
|
|
m.push(rg::RetrievalEval::context_precision({}, {DbValue("a")})).push(rg::RetrievalEval::context_precision({DbValue("x")}, {DbValue("a")}));
|
|
out("metrics " + j(m));
|
|
}
|
|
for (const std::string& text : std::vector<std::string>{"q=alpha\thits=record:m1,m2,,chunk:4", "nothing", "q=\thits=a", "a=b=c\tq= spaced \thits= x ,y ", "q=a\tq=b\thits=record:z"}) {
|
|
auto parsed = rg::RetrievalEval::parse_search_event(text);
|
|
Json v;
|
|
if (parsed) {
|
|
v = Json::array();
|
|
v.push(parsed->first).push(arr_of(parsed->second));
|
|
}
|
|
out("parse " + js(text) + " => " + j(v));
|
|
}
|
|
out("== chunks table");
|
|
std::string rid = db.query("SELECT id FROM records WHERE profile = 't' ORDER BY id LIMIT 1")[0][0].as_text();
|
|
chunks.add_chunks("record", rid, {"one", "two", "three"}, "t");
|
|
Json rows_j = Json::array();
|
|
std::vector<int64_t> ids;
|
|
for (const auto& r : db.query("SELECT id, parent_kind, parent_id, idx, text, profile FROM chunks WHERE parent_id = ? ORDER BY idx", {rid})) {
|
|
rows_j.push(row_json(r));
|
|
ids.push_back(r[0].as_int());
|
|
}
|
|
out("chunks " + j(rows_j));
|
|
auto chunk_obj = [](const std::map<int64_t, rg::ChunkRow>& m) {
|
|
Json o = Json::object();
|
|
for (const auto& kv : m) {
|
|
Json c = Json::object();
|
|
c.set("parent_kind", kv.second.parent_kind).set("parent_id", kv.second.parent_id).set("idx", kv.second.idx).set("text", kv.second.text);
|
|
o.set(std::to_string(kv.first), c);
|
|
}
|
|
return o;
|
|
};
|
|
std::vector<int64_t> ask = ids;
|
|
ask.push_back(999999);
|
|
out("by ids " + j(chunk_obj(chunks.get_chunks_by_ids(ask, "t"))));
|
|
out("by ids empty " + j(chunk_obj(chunks.get_chunks_by_ids({}, "t"))) + " other " + j(chunk_obj(chunks.get_chunks_by_ids(ids, "other"))));
|
|
chunks.add_chunks("record", rid, {}, "t");
|
|
out("after clear " + std::to_string(db.query("SELECT COUNT(*) FROM chunks WHERE parent_id = ?", {rid})[0][0].as_int()));
|
|
out("== search tool");
|
|
RagApp host(db, fts, chunks);
|
|
rg::RagTool tool(host);
|
|
std::vector<Json> calls = {
|
|
obj({{"query", ""}}), obj({{"query", "alpha"}}), obj({{"query", "alpha"}, {"limit", 3}}), obj({{"query", "alpha"}, {"limit", "2"}}), obj({{"query", "alpha"}, {"limit", "x"}}),
|
|
obj({{"query", "alpha"}, {"limit", 0}}), obj({{"query", "alpha"}, {"limit", -2}}), obj({{"query", "retrieval"}, {"kinds", arr_of({"chunk"})}}),
|
|
obj({{"query", "alpha"}, {"kinds", arr_of({"record"})}, {"expand", true}, {"limit", 20}}), obj({{"query", "alpha"}, {"kinds", arr_of({"bogus"})}}), obj({{"query", "alpha"}, {"kinds", Json::array()}}),
|
|
obj({{"query", "alpha"}, {"kinds", "record"}}), obj({{"query", "file"}, {"kinds", arr_of({"audit"})}}), obj({{"query", "message"}, {"kinds", arr_of({"event"})}}),
|
|
obj({{"query", "zebra"}, {"kinds", arr_of({"record", "chunk", "audit"})}}), obj({{"query", "nothingmatchesthis"}}), obj({{"query", std::string(200, 'x')}}), obj({{"query", " spaced "}}),
|
|
obj({{"query", "caf\xc3\xa9 r\xc3\xa9sum\xc3\xa9"}, {"limit", 50}}),
|
|
};
|
|
for (const auto& args : calls) {
|
|
std::string res;
|
|
try {
|
|
res = tool.run_search(args);
|
|
} catch (const std::exception& e) {
|
|
res = "error: " + PyStr::short_error(e.what());
|
|
}
|
|
out("search " + j(args) + " => " + js(res));
|
|
}
|
|
Json logged = Json::array();
|
|
for (const auto& r : db.query("SELECT profile, role, kind, text FROM events WHERE kind = 'search-query' ORDER BY id DESC LIMIT 25")) logged.push(row_json(r));
|
|
out("logged " + j(logged));
|
|
}
|
|
|
|
// ------------------------------------------------------------------ telegram scenarios
|
|
namespace tg = detz::telegram;
|
|
|
|
class LogRec : public detz::misc::LogSink {
|
|
public:
|
|
void event(const std::string& level, const std::string& component, const std::string& event, const std::string& message, const std::string& profile,
|
|
const std::string& bot, const std::string& chat, const Json& meta) override {
|
|
Json a = Json::array();
|
|
a.push(level).push(component).push(event).push(message).push(profile).push(bot).push(chat).push(meta);
|
|
out("LOG " + j(a));
|
|
}
|
|
void set_context(const std::string& key, const std::string& value) override { out("CTX set " + key + "=" + value); }
|
|
void clear_context(const std::string& key) override { out("CTX pop " + key); }
|
|
};
|
|
|
|
class TgAgent : public tg::TelegramAgent {
|
|
public:
|
|
std::string home_;
|
|
std::string profile() const override { return "t"; }
|
|
std::string bot() const override { return "main"; }
|
|
std::string home() const override { return home_; }
|
|
void reset_history() override { out("AGENT reset_history"); }
|
|
std::string run_turn(const std::string& text) override {
|
|
out("AGENT run_turn " + js(text) + " capture=True");
|
|
if (text.find("explode") != std::string::npos) throw std::runtime_error("agent exploded");
|
|
if (text == "long") return std::string(9000, 'L');
|
|
if (text == "empty reply") return "";
|
|
return "echo:" + text;
|
|
}
|
|
void start_scheduler() override { out("start_scheduler called"); }
|
|
};
|
|
|
|
class TgBox : public tg::BoxPort {
|
|
public:
|
|
std::string ensure_box() override {
|
|
out("BOX ensure_box");
|
|
if (std::getenv("MISC_BOX_FAIL")) throw detz::misc::BackendError("box down");
|
|
return "eng";
|
|
}
|
|
std::string box_transcribe(const std::string& engine, const std::string& audio) override {
|
|
// Python prints the audio decoded as latin-1: each byte becomes one code point
|
|
std::string latin;
|
|
for (unsigned char c : audio) {
|
|
if (c < 0x80) latin += static_cast<char>(c);
|
|
else { latin += static_cast<char>(0xC0 | (c >> 6)); latin += static_cast<char>(0x80 | (c & 0x3F)); }
|
|
}
|
|
out("BOX transcribe " + engine + " " + js(latin));
|
|
if (audio == "AUDIO") return "voice says hi";
|
|
return "error: model failed";
|
|
}
|
|
};
|
|
|
|
struct StopLoop {};
|
|
|
|
static std::string tg_api() { return std::getenv("MISC_TG_API"); }
|
|
static std::string tg_file() { return std::getenv("MISC_TG_FILE"); }
|
|
|
|
static void sc_telegram(const std::string& root) {
|
|
LogRec log;
|
|
TgAgent agent;
|
|
TgBox box;
|
|
std::string home = tai_home();
|
|
agent.home_ = home;
|
|
tg::TelegramBot bot(agent, nullptr, &box, log);
|
|
bot.set_api(tg_api(), tg_file());
|
|
out("== token");
|
|
std::string env_path = tg::TelegramBot::telegram_env_path(home);
|
|
out("env path " + js(env_path));
|
|
unsetenv("TELEGRAM_BOT_TOKEN");
|
|
out("no file => " + js(tg::TelegramBot::load_telegram_token(home)));
|
|
std::vector<std::pair<std::string, std::string>> bodies = {
|
|
{"plain", "TELEGRAM_BOT_TOKEN=abc123\n"}, {"quoted", "TELEGRAM_BOT_TOKEN='abc'\n"}, {"dquoted", "TELEGRAM_BOT_TOKEN=\"x y\"\n"}, {"spaces", "TELEGRAM_BOT_TOKEN= spaced \n"},
|
|
{"other first", "OTHER=1\nTELEGRAM_BOT_TOKEN=second\nTELEGRAM_BOT_TOKEN=third\n"}, {"none", "OTHER=1\n"}, {"empty", ""}, {"crlf", "TELEGRAM_BOT_TOKEN=win\r\n"},
|
|
{"noeq", "TELEGRAM_BOT_TOKEN\n"}, {"eq in value", "TELEGRAM_BOT_TOKEN=a=b=c\n"}, {"quotes only", "TELEGRAM_BOT_TOKEN=''\n"}, {"indented", " TELEGRAM_BOT_TOKEN=nope\n"}};
|
|
for (const auto& b : bodies) {
|
|
write(env_path, b.second);
|
|
out("file " + b.first + " => " + js(tg::TelegramBot::load_telegram_token(home)));
|
|
}
|
|
setenv("TELEGRAM_BOT_TOKEN", "fromenv", 1);
|
|
out("env wins => " + js(tg::TelegramBot::load_telegram_token(home)));
|
|
setenv("TELEGRAM_BOT_TOKEN", "", 1);
|
|
out("empty env falls back => " + js(tg::TelegramBot::load_telegram_token(home)));
|
|
unsetenv("TELEGRAM_BOT_TOKEN");
|
|
PyFs::remove(env_path);
|
|
out("== call / send");
|
|
tg::Telegram good("good", nullptr, tg_api(), tg_file());
|
|
out("getMe " + j(good.call("getMe", Json::object())));
|
|
for (const std::pair<std::string, std::string>& c : std::vector<std::pair<std::string, std::string>>{{"bad token", "bad"}, {"not json", "nojson"}}) {
|
|
try {
|
|
tg::Telegram t(c.second, nullptr, tg_api(), tg_file());
|
|
out(c.first + " => " + j(t.call("getMe", Json::object())));
|
|
} catch (const std::exception& e) {
|
|
out(c.first + " => raised " + js(PyStr::short_error(e.what())));
|
|
}
|
|
}
|
|
std::string e1(4001, 'x');
|
|
std::string em;
|
|
for (int i = 0; i < 4001; ++i) em += "\xc3\xa9";
|
|
std::string smile;
|
|
for (int i = 0; i < 4100; ++i) smile += "\xf0\x9f\x98\x80";
|
|
std::string lines;
|
|
for (int i = 0; i < 2000; ++i) lines += "line\n";
|
|
for (const std::string& text : std::vector<std::string>{"", "short", std::string(4000, 'S'), std::string(4001, 'S'), std::string(8000, 'S'), std::string(8001, 'S'), em, smile, lines}) {
|
|
tg::TelegramBot::telegram_send(good, Json(42), text);
|
|
}
|
|
tg::TelegramBot::telegram_send(good, Json("chat-as-string"), "hi");
|
|
out("== updates");
|
|
auto chat = [](const Json& id) { return obj({{"id", id}}); };
|
|
auto msg = [&](const Json& id, std::initializer_list<std::pair<const char*, Json>> extra) {
|
|
Json m = Json::object();
|
|
m.set("chat", chat(id));
|
|
for (const auto& p : extra) m.set(p.first, p.second);
|
|
return m;
|
|
};
|
|
std::vector<Json> updates;
|
|
auto upd = [&](int id, const Json& message) { updates.push_back(obj({{"update_id", id}, {"message", message}})); };
|
|
upd(1, msg(111, {{"text", "/start"}}));
|
|
upd(2, msg(111, {{"text", "/new"}}));
|
|
upd(3, msg(111, {{"text", "hello there"}}));
|
|
upd(4, msg(111, {{"text", ""}}));
|
|
upd(5, msg(111, {{"text", " "}}));
|
|
upd(6, msg(0, {{"text", "zero chat"}}));
|
|
upd(7, obj({{"text", "no chat"}}));
|
|
updates.push_back(obj({{"update_id", 8}}));
|
|
upd(9, msg(111, {}));
|
|
upd(10, msg(111, {{"text", "explode please"}}));
|
|
upd(11, msg(111, {{"text", "long"}}));
|
|
upd(12, msg(111, {{"text", "empty reply"}}));
|
|
upd(13, msg(222, {{"voice", obj({{"file_id", "f1"}})}}));
|
|
upd(14, msg(222, {{"audio", obj({{"file_id", "f1"}})}}));
|
|
upd(15, msg(222, {{"voice", obj({{"file_id", "nofile"}})}}));
|
|
upd(16, msg(222, {{"voice", obj({{"file_id", "fbad"}})}}));
|
|
upd(17, msg(222, {{"voice", obj({{"file_id", "fmodel"}})}}));
|
|
upd(18, msg(222, {{"voice", Json::object()}, {"text", "text with empty voice"}}));
|
|
upd(19, msg(222, {{"voice", obj({{"file_id", "fmissingfile"}})}}));
|
|
upd(20, msg(Json("str-chat"), {{"text", "string chat id"}}));
|
|
upd(21, msg(111, {{"text", "unicode caf\xc3\xa9 \xf0\x9f\x98\x80"}}));
|
|
for (const auto& update : updates) {
|
|
out("-- update " + j(update));
|
|
try {
|
|
bot.handle_telegram_update(good, update);
|
|
} catch (const std::exception& e) {
|
|
out("raised " + js(PyStr::short_error(e.what())));
|
|
}
|
|
}
|
|
setenv("MISC_BOX_FAIL", "1", 1);
|
|
out("-- box failing");
|
|
try {
|
|
bot.handle_telegram_update(good, obj({{"update_id", 30}, {"message", msg(5, {{"voice", obj({{"file_id", "f1"}})}})}}));
|
|
} catch (const std::exception& e) {
|
|
out("raised " + js(PyStr::short_error(e.what())));
|
|
}
|
|
unsetenv("MISC_BOX_FAIL");
|
|
out("== run_telegram_bot");
|
|
struct Run { const char* label; const char* token; };
|
|
for (const Run& r : {Run{"missing", nullptr}, Run{"bad auth", "bad"}, Run{"not json", "nojson"}, Run{"no username", "noname"}, Run{"good", "good"}}) {
|
|
unsetenv("TELEGRAM_BOT_TOKEN");
|
|
if (r.token) setenv("TELEGRAM_BOT_TOKEN", r.token, 1);
|
|
out(std::string("-- run ") + r.label);
|
|
std::cout.flush();
|
|
try {
|
|
int res = bot.run_telegram_bot(nullptr, [](int seconds) {
|
|
out("sleep " + std::to_string(seconds));
|
|
throw StopLoop();
|
|
});
|
|
out("returned " + std::to_string(res));
|
|
} catch (const StopLoop&) {
|
|
out("stopped by sleep");
|
|
}
|
|
std::cout.flush();
|
|
}
|
|
(void)root;
|
|
}
|
|
|
|
static void sc_telegram_install(const std::string& root) {
|
|
LogRec log;
|
|
TgAgent agent;
|
|
TgBox box;
|
|
FakeBox fbox;
|
|
set_box(fbox, std::string("podman"));
|
|
ins::StdIo io;
|
|
ins::InstallContext ctx = install_ctx();
|
|
ctx.telegram_api = tg_api();
|
|
ins::Installer inst(ctx, fbox, io);
|
|
std::string home = tai_home();
|
|
struct T { const char* label; const char* token; };
|
|
for (const T& t : {T{"good", "good"}, T{"bad", "bad"}, T{"nojson", "nojson"}, T{"noname", "noname"}}) {
|
|
setenv("TELEGRAM_BOT_TOKEN", t.token, 1);
|
|
out(std::string("-- install_telegram ") + t.label);
|
|
std::cout.flush();
|
|
int res = inst.install_telegram();
|
|
out("returned " + std::to_string(res));
|
|
out("unit exists " + py_bool(PyFs::exists(PyFs::home() + "/.config/systemd/user/tai-telegram.service")) + " env exists " +
|
|
py_bool(PyFs::exists(tg::TelegramBot::telegram_env_path(home))));
|
|
}
|
|
unsetenv("TELEGRAM_BOT_TOKEN");
|
|
out("-- install_telegram without token (stdin closed)");
|
|
std::cout.flush();
|
|
out("returned " + std::to_string(inst.install_telegram()));
|
|
out("-- install_telegram box failing");
|
|
set_box(fbox, std::string("podman"), "running", "venv ready", "no engine here");
|
|
setenv("TELEGRAM_BOT_TOKEN", "good", 1);
|
|
out("returned " + std::to_string(inst.install_telegram()));
|
|
set_box(fbox, std::string("podman"));
|
|
out("-- op_telegram_service");
|
|
for (const char* token : {"good", "bad", "nojson", "noname"}) {
|
|
setenv("TELEGRAM_BOT_TOKEN", token, 1);
|
|
for (const std::string mode : {"install", "install", "reinstall", "upgrade", "uninstall", "uninstall"}) {
|
|
out(std::string("service ") + token + " " + mode + " => " + js(inst.op_telegram_service(mode, home)));
|
|
}
|
|
}
|
|
unsetenv("TELEGRAM_BOT_TOKEN");
|
|
write(tg::TelegramBot::telegram_env_path(home), "TELEGRAM_BOT_TOKEN=good\n");
|
|
out("service from env file => " + js(inst.op_telegram_service("install", home)));
|
|
out("-- uninstall_telegram");
|
|
out("returned " + std::to_string(inst.uninstall_telegram()));
|
|
set_box(fbox, std::nullopt);
|
|
out("returned noengine " + std::to_string(inst.uninstall_telegram()));
|
|
(void)root;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ release / audit scenarios
|
|
using detz::install::SelfRelease;
|
|
|
|
class NoRecords : public ins::FileRecordPort {
|
|
public:
|
|
void upsert_file_record(const std::string&, const std::string&, const std::optional<int64_t>&) override {} // records table: store module
|
|
};
|
|
|
|
class RelHost : public ins::ReleaseHost {
|
|
public:
|
|
std::vector<std::string> approvals;
|
|
bool approve = true;
|
|
std::string script, home_;
|
|
ins::AuditVault* vault_ = nullptr;
|
|
bool ask_approval(const std::string& w) override { approvals.push_back(w); return approve; }
|
|
std::string home() const override { return home_; }
|
|
std::string audit_actor() const override { return "main"; }
|
|
ins::AuditPort& store() override { return *vault_; }
|
|
std::string script_file() const override { return script; }
|
|
};
|
|
|
|
static std::string copy_file_bytes(const std::string& from, const std::string& to) {
|
|
write(to, PyFs::read_bytes(from));
|
|
return to;
|
|
}
|
|
|
|
static void sc_release(const std::string& root) {
|
|
SqliteDb db(tai_home() + "/memory.db", 30, true);
|
|
ins::AuditVault vault(db, "t");
|
|
NoRecords records;
|
|
std::string script = copy_file_bytes(install_ctx().script_path, root + "/script.py");
|
|
std::string folder = root + "/backups";
|
|
out("== versions");
|
|
struct V { const char* cur; const char* part; };
|
|
for (const V& v : {V{"1.2.3", "patch"}, V{"1.2.3", "minor"}, V{"1.2.3", "major"}, V{"9.9.9", "patch"}, V{"1.2.3", "bogus"}, V{"1.2", "patch"}, V{"a.b.c", "patch"}, V{"1.2.3.4", "patch"}}) {
|
|
try {
|
|
out(std::string("next ") + v.cur + " " + v.part + " => " + SelfRelease::next_version(v.cur, v.part));
|
|
} catch (const std::exception& e) {
|
|
out(std::string("next ") + v.cur + " " + v.part + " => raised " + js(e.what()));
|
|
}
|
|
}
|
|
out("== bump");
|
|
for (const char* part : {"patch", "minor", "major", "patch"}) {
|
|
auto r = SelfRelease::release_bump(script, part);
|
|
Json a = Json::array();
|
|
a.push(r.first).push(r.second);
|
|
out(std::string("bump ") + part + " => " + j(a));
|
|
}
|
|
{
|
|
std::string text = PyFs::read_text(script);
|
|
std::vector<std::string> vl;
|
|
for (const auto& l : PyStr::splitlines(text)) {
|
|
if (l.rfind("VERSION = ", 0) == 0) vl.push_back(l);
|
|
}
|
|
out("version line " + j(arr_of(vl)));
|
|
}
|
|
write(root + "/nov.py", "print('x')\n");
|
|
for (const std::string& f : {root + "/nov.py", root + "/missing.py"}) {
|
|
try {
|
|
SelfRelease::release_bump(f, "patch");
|
|
} catch (const std::exception& e) {
|
|
out("bump error " + js(replace_root(e.what(), root)));
|
|
}
|
|
}
|
|
write(root + "/crlf.py", "a\r\nVERSION = \"1.0.0\"\r\nb\r\n");
|
|
{
|
|
auto r = SelfRelease::release_bump(root + "/crlf.py", "patch");
|
|
Json a = Json::array();
|
|
a.push(r.first).push(r.second);
|
|
out("crlf bump " + j(a) + " " + js(PyFs::read_bytes(root + "/crlf.py")));
|
|
}
|
|
out("== snapshots and pruning");
|
|
auto snap = SelfRelease::snapshot_self(script, folder, "9.9.9");
|
|
out("snapshot " + js(PyFs::basename(snap.first)) + " " + snap.second);
|
|
out("has digest " + py_bool(SelfRelease::backup_has_digest(folder, snap.second)) + " " + py_bool(SelfRelease::backup_has_digest(folder, "deadbeef")));
|
|
out("has digest nofolder " + py_bool(SelfRelease::backup_has_digest(root + "/nofolder", snap.second)));
|
|
for (int i = 0; i < 14; ++i) {
|
|
char name[128];
|
|
std::snprintf(name, sizeof name, "/tai-0.0.%d-20200101T000000Z-%08d.py", i, i);
|
|
write(folder + name, "x");
|
|
struct timespec ts[2] = {{1000 + i, 0}, {1000 + i, 0}};
|
|
utimensat(AT_FDCWD, (folder + name).c_str(), ts, 0);
|
|
}
|
|
write(folder + "/notes.txt", "keep");
|
|
out("pruned " + std::to_string(SelfRelease::prune_backups(folder)));
|
|
out("left " + PyStr::replace_all(j(arr_of(PyFs::listdir_sorted(folder))), PyFs::basename(snap.first), "<SNAP>"));
|
|
out("prune missing " + std::to_string(SelfRelease::prune_backups(root + "/nofolder")));
|
|
out("== ensure_self_backup");
|
|
PyFs::rmtree(folder);
|
|
auto first = SelfRelease::ensure_self_backup(tai_home(), &vault, &records, script, "1.44.2");
|
|
out("ensure first " + (first ? js(PyFs::basename(*first)) : std::string("null")));
|
|
auto second = SelfRelease::ensure_self_backup(tai_home(), &vault, &records, script, "1.44.2");
|
|
out("ensure second " + (second ? js(PyFs::basename(*second)) : std::string("null")));
|
|
{
|
|
Json rows = Json::array();
|
|
for (const auto& r : db.query("SELECT actor, action, path, message, old_size, new_size, tags FROM audit ORDER BY id")) rows.push(row_json(r));
|
|
out("audit " + j(rows));
|
|
}
|
|
out("== do_release");
|
|
try {
|
|
SelfRelease::do_release(vault, script, folder, "patch", " ");
|
|
} catch (const std::exception& e) {
|
|
out("blank message " + js(e.what()));
|
|
}
|
|
{
|
|
auto r = SelfRelease::do_release(vault, script, folder, "minor", " ship it ", "tester");
|
|
Json a = Json::array();
|
|
a.push(r.old_version).push(r.new_version).push(PyFs::basename(r.dest)).push(r.row_id);
|
|
out("do_release " + j(a));
|
|
}
|
|
out("== release tool");
|
|
RelHost host;
|
|
host.script = script;
|
|
host.home_ = tai_home();
|
|
host.vault_ = &vault;
|
|
ins::ReleaseTool tool(host);
|
|
auto rel = [&](const Json& args) {
|
|
try {
|
|
return tool.run_release(args);
|
|
} catch (const std::exception& e) {
|
|
return "error: " + PyStr::short_error(e.what());
|
|
}
|
|
};
|
|
for (const Json& args : std::vector<Json>{obj({{"part", ""}, {"message", "m"}}), obj({{"part", "patch"}, {"message", ""}}), obj({{"part", "huge"}, {"message", "m"}}),
|
|
obj({{"part", "Patch"}, {"message", "tool release"}}), obj({{"part", "major"}, {"message", std::string(300, 'm')}})}) {
|
|
out("release " + j(args) + " => " + js(replace_root(rel(args), root)));
|
|
}
|
|
host.approve = false;
|
|
out("release denied => " + js(rel(obj({{"part", "patch"}, {"message", "no"}}))));
|
|
host.approve = true;
|
|
write(script, "nothing here\n");
|
|
out("release no version => " + js(rel(obj({{"part", "patch"}, {"message", "m"}}))));
|
|
out("approvals " + j(arr_of(host.approvals)));
|
|
out("== capped helpers");
|
|
{
|
|
auto c0 = SelfRelease::capped_text(std::nullopt);
|
|
auto c1 = SelfRelease::capped_text(std::string("abc"));
|
|
std::string e;
|
|
for (int i = 0; i < 20001; ++i) e += "\xc3\xa9";
|
|
auto c2 = SelfRelease::capped_text(e);
|
|
auto c3 = SelfRelease::capped_text(std::string(20001, 'x'));
|
|
auto pair_json = [](const std::pair<std::optional<std::string>, std::optional<int64_t>>& p) {
|
|
Json a = Json::array();
|
|
a.push(p.first ? Json(*p.first) : Json()).push(p.second ? Json(*p.second) : Json());
|
|
return a;
|
|
};
|
|
Json all = Json::array();
|
|
all.push(pair_json(c0)).push(pair_json(c1)).push(Json(*c2.second));
|
|
all.push(Json(PyStr::suffix(*c3.first, 60))).push(SelfRelease::trunc_marker(5));
|
|
out("capped " + j(all));
|
|
}
|
|
std::string big = "line\r\nsecond\rthird\n";
|
|
for (int i = 0; i < 30000; ++i) big += "\xc3\xa9";
|
|
write(root + "/big.txt", big);
|
|
for (int cap : {20000, 50, 100000}) {
|
|
auto r = SelfRelease::read_capped(root + "/big.txt", static_cast<std::size_t>(cap));
|
|
out("read_capped " + std::to_string(cap) + " => " + (r.text ? js(PyStr::prefix(*r.text, 40)) : std::string("null")) + " " + (r.size ? std::to_string(*r.size) : std::string("null")));
|
|
}
|
|
write(root + "/bad.bin", std::string("ok \xff\xfe bad \xc3 end"));
|
|
{
|
|
auto r = SelfRelease::read_capped(root + "/bad.bin");
|
|
Json a = Json::array();
|
|
a.push(r.text ? Json(*r.text) : Json()).push(r.size ? Json(*r.size) : Json());
|
|
out("read_capped invalid " + j(a));
|
|
}
|
|
{
|
|
auto r = SelfRelease::read_capped(root + "/missing");
|
|
Json a = Json::array();
|
|
a.push(Json()).push(Json());
|
|
out("read_capped missing " + j(a));
|
|
(void)r;
|
|
}
|
|
}
|
|
|
|
class AuditApp : public ins::AuditHost {
|
|
public:
|
|
std::vector<std::string> approvals;
|
|
bool approve = true;
|
|
ins::AuditVault* vault_ = nullptr;
|
|
rg::FtsSearch* fts_ = nullptr;
|
|
bool ask_approval(const std::string& w) override { approvals.push_back(w); return approve; }
|
|
std::string active_profile() const override { return "t"; }
|
|
ins::AuditVault& vault() override { return *vault_; }
|
|
rg::FtsSearch& fts() override { return *fts_; }
|
|
void read_files_add(const std::string&) override {}
|
|
void audit_file(const std::string& action, const std::string& path, const std::optional<std::string>& o, const std::optional<std::string>& n, const std::string& message) override {
|
|
vault_->audit_event_full("main", action, path, message, o, n, {"file", action}, std::nullopt, std::nullopt, "t");
|
|
}
|
|
void upsert_file_record(const std::string&, const std::string&, const std::string&) override {} // records table: store module
|
|
void note_diff(const std::string&, const std::optional<std::string>&, const std::optional<std::string>&) override {}
|
|
std::string box_restore(const std::string&, const std::string&, const std::string&) override { return "box restore unsupported in this test"; }
|
|
};
|
|
|
|
static void sc_audit(const std::string& root) {
|
|
SqliteDb db(tai_home() + "/memory.db", 30, true);
|
|
ins::AuditVault vault(db, "t");
|
|
rg::FtsSearch fts(db, "t");
|
|
AuditApp app;
|
|
app.vault_ = &vault;
|
|
app.fts_ = &fts;
|
|
ins::AuditTool tool(app);
|
|
auto call = [&](const std::string& name, const Json& args) {
|
|
std::string res;
|
|
try {
|
|
res = name == "audit" ? tool.run_audit(args) : name == "tags" ? tool.run_tags(args) : tool.run_restore(args);
|
|
} catch (const std::exception& e) {
|
|
res = "error: " + PyStr::short_error(e.what());
|
|
}
|
|
out(name + " " + j(args) + " => " + js(replace_root(res, root)));
|
|
};
|
|
out("== audit");
|
|
for (const Json& a : std::vector<Json>{Json::object(), obj({{"limit", 3}}), obj({{"path", "area/a.txt"}}), obj({{"tag", "file"}}), obj({{"tag", "nomatch"}}), obj({{"query", "alpha"}}),
|
|
obj({{"query", "write"}}), obj({{"query", "zzzz"}}), obj({{"limit", "x"}}), obj({{"limit", 500}}), obj({{"query", "unicode"}})})
|
|
call("audit", a);
|
|
out("== tags");
|
|
for (const Json& a : std::vector<Json>{Json::object(), obj({{"prefix", "al"}}), obj({{"prefix", "A_"}}), obj({{"prefix", "a%"}}), obj({{"prefix", "zzz"}}), obj({{"limit", 1}}),
|
|
obj({{"limit", "x"}}), obj({{"prefix", " Al "}})})
|
|
call("tags", a);
|
|
out("== restore");
|
|
for (const Json& a : std::vector<Json>{Json::object(), obj({{"id", "x"}}), obj({{"id", 9999}}), obj({{"id", 6}}), obj({{"id", 7}}), obj({{"id", 1}}), obj({{"id", 3}}), obj({{"id", 4}}),
|
|
obj({{"id", 5}}), obj({{"id", 12}}), obj({{"id", 10}}), obj({{"id", 11}})})
|
|
call("restore", a);
|
|
for (const std::string rel : {"a.txt", "sub/deep/r.txt", "u.txt"}) {
|
|
std::string path = "area/" + rel;
|
|
out("file " + rel + " " + (PyFs::exists(path) ? js(read(path)) : std::string("null")));
|
|
}
|
|
app.approve = false;
|
|
call("restore", obj({{"id", 2}}));
|
|
app.approve = true;
|
|
Json ap = Json::array();
|
|
for (const auto& a : app.approvals) ap.push(replace_root(a, root));
|
|
out("approvals " + j(ap));
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc < 3) return 2;
|
|
std::string name = argv[1], root = argv[2];
|
|
bool prepare = argc > 3 && std::string(argv[3]) == "prepare";
|
|
const char* data_dir = std::getenv("MISC_DATA_DIR");
|
|
DATA_DIR = data_dir ? data_dir : "tests/misc_data";
|
|
if (chdir((root + "/proj").c_str()) != 0) return 2;
|
|
if (prepare) return 0; // C++ never prepares: the Python side builds fixtures with the real tai.py
|
|
static const std::map<std::string, std::function<void(const std::string&)>> scenarios = {
|
|
{"json", sc_json}, {"datetime", sc_datetime}, {"chunk", sc_chunk}, {"fts", sc_fts},
|
|
{"skills_data", sc_skills_data}, {"skills_files", sc_skills_files}, {"skills_ops", sc_skills_ops},
|
|
{"install_hooks", sc_install_hooks}, {"install_hooks_noshell", sc_install_hooks}, {"install_ops", sc_install_ops},
|
|
{"install_noengine", sc_install_noengine}, {"vault_line", sc_vault_line}, {"sched", sc_sched}, {"rag", sc_rag}, {"release", sc_release}, {"audit", sc_audit}, {"telegram", sc_telegram}, {"telegram_install", sc_telegram_install},
|
|
};
|
|
auto it = scenarios.find(name);
|
|
if (it == scenarios.end()) {
|
|
std::cerr << "unknown scenario " << name << "\n";
|
|
return 2;
|
|
}
|
|
it->second(root);
|
|
return 0;
|
|
}
|