Add safe bank failure diagnostics for T5 and T6

Address #14 with request correlation, bounded parser reasons, startup version reporting, and rate-limited storage and settlement logs. Preserve accounting and API identity contracts, extend portable tests, and update release versions and rollout guidance.
This commit is contained in:
2026-10-13 10:02:36 -04:00
parent ab989395f7
commit 463b4bcc51
19 changed files with 804 additions and 78 deletions
+23
View File
@@ -104,6 +104,29 @@ int main(int argc, char** argv)
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);
// Every validation failure exposes a fixed reason and clears response data.
const auto invalid = [](const std::string& text, const std::string& reason)
{
const auto result = parse_acknowledgement(text);
assert(!result.valid && result.reason == reason);
assert(result.state.empty() && result.code.empty() && result.id.empty());
};
assert(ack.reason == "ok");
invalid("", "empty_body");
invalid(std::string(0x5000, 'x'), "body_limit");
invalid("secret-token-not-json", "invalid_json");
invalid("null", "schema");
invalid("{}", "schema");
bad = valid; bad["code"] = std::string(65, 'x'); invalid(bad.dump(), "field_limit");
bad = valid; bad["state"] = "secret-state"; invalid(bad.dump(), "state");
bad = valid; bad["amount"] = -1; invalid(bad.dump(), "amount_range");
bad = valid; bad["ok"] = false; invalid(bad.dump(), "ok_state");
bad = valid; bad["request_id"] = ""; invalid(bad.dump(), "missing_id");
bad = valid; bad["amount"] = 0; invalid(bad.dump(), "zero_transfer");
bad = valid; bad["code"] = "private-token\nAuthorization: secret";
// Compatibility: valid but unknown API codes remain available to callers;
// scripts permit only fixed known codes in diagnostics.
assert(parse_acknowledgement(bad.dump()).valid);
{ std::ofstream stream(file); stream << "corrupt"; }
must_fail([&]{queue.load();});
must_fail([&]{queue.store(body("three","cancel"));});
+38
View File
@@ -0,0 +1,38 @@
// Executed with the actual registration adapter and portable engine interfaces.
#include <fstream>
int main()
{
bank::component component;
component.on_startup(nullptr);
const auto call = [](const std::string& name, std::vector<std::string> args = {})
{
return gsc::function::functions.at(name)(args);
};
assert(call("bankversion").as<std::string>() == "1.2.0");
auto ack = call("bankresponse", {"private-secret-invalid-json"}).as<scripting::array>();
assert(!ack["valid"].as<bool>());
assert(ack["reason"].as<std::string>() == "invalid_json");
assert(ack["code"].as<std::string>().empty());
assert(call("bankload", {"server", "endpoint"}).is<scripting::array>());
for (int i = 0; i < 2; ++i)
assert(!call("bankstore", {"server", "endpoint", "private-secret Authorization"}).as<bool>());
const std::string body = R"({"server":"server","request_id":"one","decision":"cancel"})";
assert(call("bankstore", {"server", "endpoint", body}).as<bool>());
const auto root = bank::spool("server", "endpoint");
assert(root.load().size() == 1);
// Locate only the test process's temporary outbox.
std::filesystem::path file;
for (const auto& entry : std::filesystem::recursive_directory_iterator("tws-bank"))
if (entry.path().extension() == ".json") file = entry.path();
assert(!file.empty());
{ std::ofstream stream(file); stream << "private-secret-corrupt-journal"; }
for (int i = 0; i < 2; ++i)
{
assert(call("bankload", {"server", "endpoint"}).get_raw().type == game::SCRIPT_NONE);
assert(!call("bankremove", {"server", "endpoint", "one"}).as<bool>());
}
std::filesystem::remove(file);
assert(call("bankload", {"server", "endpoint"}).is<scripting::array>());
assert(call("bankremove", {"server", "endpoint", "one"}).as<bool>());
}