Add persistent cross-game player bank (Refs #3)

This commit is contained in:
2026-10-12 20:45:41 -04:00
parent eda2c7e76d
commit 9cce0be438
19 changed files with 1646 additions and 15 deletions
+105
View File
@@ -0,0 +1,105 @@
#include "bank.hpp"
#include <json.hpp>
#include <cassert>
#include <fstream>
#include <iostream>
namespace fs = std::filesystem;
using utils::bank::outbox;
using utils::bank::parse_acknowledgement;
const std::string server = "test-server", endpoint = "https://example.test/api/bank";
std::string body(const std::string& id, const std::string& decision)
{
return nlohmann::json{{"server",server},{"guid","18446744073709551615"},
{"match","match"},{"request_id",id},{"operation","deposit"},
{"amount",2000},{"capacity",1000000},{"decision",decision}}.dump();
}
template <typename F> void must_fail(F action)
{
bool failed = false;
try { action(); } catch (const std::exception&) { failed = true; }
assert(failed);
}
int main(int argc, char** argv)
{
assert(argc >= 2);
const fs::path root(argv[1]);
const outbox queue(root,server,endpoint);
if (argc == 3)
{
const std::string mode(argv[2]);
if (mode == "produce")
{
queue.store(body("uncertain-prepare","cancel"));
queue.store(body("applied-points","cancel"));
queue.store(body("applied-points","commit"));
}
else if (mode == "recover")
{
const auto entries = queue.load();
assert(entries.size() == 2);
assert(entries[0].decision == "cancel" && entries[1].decision == "commit");
for (const auto& entry : entries) queue.erase(entry.id);
}
else assert(mode == "empty" && queue.load().empty());
return 0;
}
fs::create_directories(root);
assert(queue.load().empty());
queue.store(body("one","cancel"));
queue.store(body("one","cancel"));
assert(queue.load().size() == 1);
queue.store(body("one","commit"));
queue.store(body("one","commit"));
assert(queue.load()[0].decision == "commit");
must_fail([&]{queue.store(body("one","cancel"));});
auto changed = nlohmann::json::parse(body("one","commit"));
changed["amount"] = 100;
must_fail([&]{queue.store(changed.dump());});
assert(queue.load()[0].body == body("one","commit"));
const outbox isolated(root,"another-server",endpoint);
assert(isolated.load().empty());
const outbox another_endpoint(root,server,endpoint+"/other");
assert(another_endpoint.load().empty());
// An interrupted temp write cannot replace the committed decision.
const auto file = fs::directory_iterator(root)->path();
{ std::ofstream stream(file.string()+".tmp"); stream << "incomplete"; }
assert(queue.load()[0].decision == "commit");
fs::remove(file.string()+".tmp");
// A failed replacement preserves cancellation and must stop points moving.
queue.erase("one");
queue.store(body("two","cancel"));
fs::create_directory(file.string()+".tmp");
must_fail([&]{queue.store(body("two","commit"));});
assert(queue.load()[0].decision == "cancel");
fs::remove(file.string()+".tmp");
queue.erase("two");
queue.erase("two");
assert(queue.load().empty());
auto valid = nlohmann::json{{"ok",true},{"state","prepared"},{"request_id","one"},
{"code","ok"},{"amount",2000},{"balance",3000}};
const auto ack = parse_acknowledgement(valid.dump());
assert(ack.valid && ack.amount == 2000 && ack.balance == 3000 && ack.id == "one");
for (const auto& field : {"amount","balance"})
for (const auto& bad : {nlohmann::json(-1),nlohmann::json(1.5),nlohmann::json("100"),nlohmann::json(1000001)})
{
auto value = valid; value[field] = bad;
assert(!parse_acknowledgement(value.dump()).valid);
}
auto bad = valid; bad["ok"] = false;
assert(!parse_acknowledgement(bad.dump()).valid);
bad = valid; bad["request_id"] = "";
assert(!parse_acknowledgement(bad.dump()).valid);
bad = valid; bad["state"] = "unknown";
assert(!parse_acknowledgement(bad.dump()).valid);
for (const auto& text : {"", "<html>bad gateway</html>", "{", "null", "{\"ok\":true}"})
assert(!parse_acknowledgement(text).valid);
assert(!parse_acknowledgement(std::string(0x5000,' ')+valid.dump()).valid);
{ std::ofstream stream(file); stream << "corrupt"; }
must_fail([&]{queue.load();});
must_fail([&]{queue.store(body("three","cancel"));});
must_fail([&]{queue.erase("three");});
fs::remove(file);
std::cout << "bank decisions, corruption, cancellation, response guards passed\n";
}