Sage scoping finding (owner directive: Glove must stay general): the reusable seam was named after a single harness. Rename the virtual channel interface to wallet_status_projection_channel and the options struct to wallet_status_projection_options; any guest harness may now implement the seam. The concrete wallet_pi_status_adapter keeps its Pi name with the explicit "Pi-shaped projection only" caveat, keeping the general mechanism honest about which projections are Pi-specific. No behavior change; mechanical rename. Validation (umask 0022): dev CTest 79/79, ASan/UBSan 79/79, TSan 79/79; clang-format clean; git diff --check clean.
600 lines
24 KiB
C++
600 lines
24 KiB
C++
#include "wallet_pi_status_adapter.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <expected>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <stdexcept>
|
|
#include <stop_token>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
#define REQUIRE(condition) \
|
|
do { \
|
|
if (!(condition)) { \
|
|
std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", #condition, __FILE__, __LINE__); \
|
|
return 1; \
|
|
} \
|
|
} while (false)
|
|
|
|
using steady_time = std::chrono::steady_clock::time_point;
|
|
|
|
enum class authority_mode : std::uint8_t { fresh, disconnected, unavailable, stale, mismatch };
|
|
enum class channel_mode : std::uint8_t {
|
|
delegate,
|
|
malformed,
|
|
wrong_id,
|
|
mutation,
|
|
oversized,
|
|
transport_error,
|
|
throws,
|
|
};
|
|
|
|
struct fake_state {
|
|
std::atomic<authority_mode> authority{authority_mode::fresh};
|
|
std::atomic<channel_mode> channel{channel_mode::delegate};
|
|
std::atomic<int> calls{0};
|
|
std::atomic<int> active{0};
|
|
std::atomic<int> maximum_active{0};
|
|
std::atomic<int> delay_ms{2};
|
|
std::mutex mutex;
|
|
std::vector<std::string> requests;
|
|
std::vector<steady_time> starts;
|
|
std::vector<steady_time> deadlines;
|
|
};
|
|
|
|
[[nodiscard]] auto session() -> glove::control::session_record {
|
|
return {
|
|
.schema_version = 1,
|
|
.session_id = "private-session-id",
|
|
.controller_plan_digest = std::string(64U, 'a'),
|
|
.plan_content_digest = std::string(64U, 'b'),
|
|
.state = glove::control::session_state::running,
|
|
.policy_revision = 1,
|
|
.expires_at_ms = 20'000U,
|
|
.created_at_ms = 1'000U,
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] auto policy() -> glove::control::wallet_status_bridge_policy {
|
|
return {
|
|
.schema_version = 1,
|
|
.tool_policy_id = "private-wallet-status-policy",
|
|
.wallet_server_alias = "synthetic-wallet-host",
|
|
.wallet_server_node_digest = std::string(64U, 'c'),
|
|
.allowed_chain_ids = {8453U},
|
|
.maximum_status_age_ms = 5'000U,
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] auto binding() -> glove::control::wallet_status_plan_binding {
|
|
const auto current = session();
|
|
return {
|
|
.session_id = current.session_id,
|
|
.controller_plan_digest = current.controller_plan_digest,
|
|
.plan_content_digest = current.plan_content_digest,
|
|
.policy_revision = current.policy_revision,
|
|
.expires_at_ms = current.expires_at_ms,
|
|
.generation = 1U,
|
|
};
|
|
}
|
|
|
|
class fixed_clock final : public glove::control::wallet_status_clock {
|
|
public:
|
|
[[nodiscard]] auto now() noexcept -> glove::control::wallet_status_time override {
|
|
return {.unix_time_ms = 10'000U, .monotonic_time = std::chrono::steady_clock::now()};
|
|
}
|
|
};
|
|
|
|
class plan_authority final : public glove::control::wallet_status_plan_authority {
|
|
public:
|
|
explicit plan_authority(std::shared_ptr<fake_state> state) : state_(std::move(state)) {}
|
|
|
|
[[nodiscard]] auto snapshot(steady_time /*deadline*/) const
|
|
-> glove::control::wallet_status_authority_result<
|
|
glove::control::wallet_status_plan_snapshot> override {
|
|
if (state_->authority.load() == authority_mode::unavailable) {
|
|
return std::unexpected(glove::control::wallet_status_authority_error::unavailable);
|
|
}
|
|
return glove::control::wallet_status_plan_snapshot{
|
|
.binding = binding(),
|
|
.session = session(),
|
|
.policy = policy(),
|
|
.adapter_command_digest = std::string(64U, 'd'),
|
|
};
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<fake_state> state_;
|
|
};
|
|
|
|
class readiness_authority final : public glove::control::wallet_status_readiness_authority {
|
|
public:
|
|
[[nodiscard]] auto snapshot(
|
|
const glove::control::wallet_status_plan_binding& current, steady_time /*deadline*/
|
|
) const
|
|
-> glove::control::wallet_status_authority_result<
|
|
glove::control::wallet_status_readiness_snapshot> override {
|
|
return glove::control::wallet_status_readiness_snapshot{
|
|
.binding = current,
|
|
.audit_generation = 1U,
|
|
.journal_generation = 1U,
|
|
};
|
|
}
|
|
};
|
|
|
|
class adapter_authority final : public glove::control::wallet_status_adapter_authority {
|
|
public:
|
|
[[nodiscard]] auto snapshot(
|
|
const glove::control::wallet_status_plan_binding& current, steady_time /*deadline*/
|
|
) const
|
|
-> glove::control::wallet_status_authority_result<
|
|
glove::control::wallet_status_adapter_snapshot> override {
|
|
return glove::control::wallet_status_adapter_snapshot{
|
|
.binding = current,
|
|
.adapter_command_digest = std::string(64U, 'd'),
|
|
};
|
|
}
|
|
};
|
|
|
|
class server_authority final : public glove::control::wallet_status_server_authority {
|
|
public:
|
|
explicit server_authority(std::shared_ptr<fake_state> state) : state_(std::move(state)) {}
|
|
|
|
[[nodiscard]] auto snapshot(
|
|
const glove::control::wallet_status_plan_binding& current, steady_time /*deadline*/
|
|
) const
|
|
-> glove::control::wallet_status_authority_result<
|
|
glove::control::wallet_status_server_snapshot> override {
|
|
const auto mode = state_->authority.load();
|
|
return glove::control::wallet_status_server_snapshot{
|
|
.binding = current,
|
|
.wallet_server_node_digest = std::string(64U, 'c'),
|
|
.observation = {
|
|
.connected = mode != authority_mode::disconnected,
|
|
.observed_at_ms = mode == authority_mode::stale ? 1U : 9'000U,
|
|
.wallet_server_alias = mode == authority_mode::mismatch ? "wrong-wallet-host"
|
|
: "synthetic-wallet-host",
|
|
.allowed_chain_ids = {8453U},
|
|
},
|
|
};
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<fake_state> state_;
|
|
};
|
|
|
|
[[nodiscard]] auto make_bridge(std::shared_ptr<fake_state> state)
|
|
-> std::unique_ptr<glove::control::wallet_status_bridge> {
|
|
return std::make_unique<glove::control::wallet_status_bridge>(
|
|
std::make_unique<plan_authority>(state),
|
|
std::make_unique<readiness_authority>(),
|
|
std::make_unique<adapter_authority>(),
|
|
std::make_unique<server_authority>(std::move(state)),
|
|
std::make_unique<fixed_clock>()
|
|
);
|
|
}
|
|
|
|
class fake_channel final : public glove::control::wallet_status_projection_channel {
|
|
public:
|
|
explicit fake_channel(std::shared_ptr<fake_state> state)
|
|
: state_(std::move(state)), bridge_(make_bridge(state_)) {}
|
|
|
|
[[nodiscard]] auto
|
|
exchange_status(std::string_view request, steady_time deadline, const std::stop_token& stop)
|
|
-> std::expected<std::string, std::string> override {
|
|
state_->calls.fetch_add(1);
|
|
const auto active = state_->active.fetch_add(1) + 1;
|
|
auto maximum = state_->maximum_active.load();
|
|
while (active > maximum && !state_->maximum_active.compare_exchange_weak(maximum, active)) {
|
|
}
|
|
|
|
struct active_guard {
|
|
std::shared_ptr<fake_state> state;
|
|
|
|
~active_guard() { state->active.fetch_sub(1); }
|
|
} guard{state_};
|
|
|
|
{
|
|
const std::scoped_lock lock{state_->mutex};
|
|
state_->requests.emplace_back(request);
|
|
state_->starts.push_back(std::chrono::steady_clock::now());
|
|
state_->deadlines.push_back(deadline);
|
|
}
|
|
if (stop.stop_requested() || std::chrono::steady_clock::now() >= deadline) {
|
|
return std::unexpected(std::string{"fake channel deadline"});
|
|
}
|
|
const auto delay_until =
|
|
std::chrono::steady_clock::now() + std::chrono::milliseconds{state_->delay_ms.load()};
|
|
while (std::chrono::steady_clock::now() < delay_until) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (stop.stop_requested() || now >= deadline) {
|
|
return std::unexpected(std::string{"fake channel deadline"});
|
|
}
|
|
std::this_thread::sleep_until(
|
|
std::min({delay_until, deadline, now + std::chrono::milliseconds{1}})
|
|
);
|
|
}
|
|
if (stop.stop_requested() || std::chrono::steady_clock::now() >= deadline) {
|
|
return std::unexpected(std::string{"fake channel deadline"});
|
|
}
|
|
switch (state_->channel.load()) {
|
|
case channel_mode::malformed:
|
|
return std::string{"{"};
|
|
case channel_mode::oversized:
|
|
return std::string(glove::control::max_wallet_status_frame_bytes + 1U, 'x');
|
|
case channel_mode::transport_error:
|
|
return std::unexpected(std::string{"private transport detail"});
|
|
case channel_mode::throws:
|
|
throw std::runtime_error{"private channel exception"};
|
|
case channel_mode::delegate:
|
|
case channel_mode::wrong_id:
|
|
case channel_mode::mutation:
|
|
break;
|
|
}
|
|
auto response = bridge_->handle_request(request, deadline);
|
|
if (!response) {
|
|
return std::unexpected(response.error());
|
|
}
|
|
if (state_->channel.load() == channel_mode::wrong_id) {
|
|
const auto id_start = response->find(R"("id":")");
|
|
if (id_start != std::string::npos) {
|
|
const auto value_start = id_start + 6U;
|
|
const auto value_end = response->find('"', value_start);
|
|
response->replace(value_start, value_end - value_start, "wrong-id");
|
|
}
|
|
}
|
|
if (state_->channel.load() == channel_mode::mutation) {
|
|
const auto empty = response->find(R"("mutating_actions":[])");
|
|
if (empty != std::string::npos) {
|
|
response->replace(empty, 21U, R"("mutating_actions":["sign"])");
|
|
}
|
|
}
|
|
return response;
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<fake_state> state_;
|
|
std::unique_ptr<glove::control::wallet_status_bridge> bridge_;
|
|
};
|
|
|
|
[[nodiscard]] auto enabled_options() -> glove::control::wallet_status_projection_options {
|
|
return {
|
|
.enabled = true,
|
|
.request_timeout = std::chrono::milliseconds{250},
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] auto make_adapter(
|
|
const std::shared_ptr<fake_state>& state,
|
|
glove::control::wallet_status_projection_options options = enabled_options()
|
|
) -> std::unique_ptr<glove::control::wallet_pi_status_adapter> {
|
|
return std::make_unique<glove::control::wallet_pi_status_adapter>(
|
|
std::make_unique<fake_channel>(state), options
|
|
);
|
|
}
|
|
|
|
[[nodiscard]] auto deadline() -> steady_time {
|
|
return std::chrono::steady_clock::now() + std::chrono::seconds{1};
|
|
}
|
|
|
|
[[nodiscard]] auto run() -> int {
|
|
using glove::control::wallet_pi_status_adapter;
|
|
using glove::mcp::tool_call_status;
|
|
|
|
auto disabled_state = std::make_shared<fake_state>();
|
|
auto disabled_options = enabled_options();
|
|
disabled_options.enabled = false;
|
|
auto disabled = make_adapter(disabled_state, disabled_options);
|
|
REQUIRE(disabled->advertised_tools(deadline()).empty());
|
|
REQUIRE(
|
|
disabled->invoke("wallet_status", R"({"sign":true})", deadline()).status ==
|
|
tool_call_status::tool_not_found
|
|
);
|
|
REQUIRE(disabled_state->calls.load() == 0);
|
|
|
|
wallet_pi_status_adapter channel_less{nullptr, enabled_options()};
|
|
REQUIRE(channel_less.advertised_tools(deadline()).empty());
|
|
|
|
for (const auto invalid_timeout : {
|
|
std::chrono::milliseconds::zero(),
|
|
std::chrono::milliseconds{glove::control::max_wallet_status_request_ttl_ms + 1U},
|
|
}) {
|
|
auto invalid_state = std::make_shared<fake_state>();
|
|
auto invalid_options = enabled_options();
|
|
invalid_options.request_timeout = invalid_timeout;
|
|
auto invalid_adapter = make_adapter(invalid_state, invalid_options);
|
|
REQUIRE(invalid_adapter->advertised_tools(deadline()).empty());
|
|
REQUIRE(invalid_state->calls.load() == 0);
|
|
}
|
|
|
|
auto state = std::make_shared<fake_state>();
|
|
auto adapter = make_adapter(state);
|
|
auto tools = adapter->advertised_tools(deadline());
|
|
REQUIRE(tools.size() == 1U);
|
|
REQUIRE(tools.front().name == "wallet_status");
|
|
REQUIRE(tools.front().description == "Read redacted wallet connectivity status.");
|
|
REQUIRE(
|
|
tools.front().input_schema_json ==
|
|
R"({"type":"object","properties":{},"additionalProperties":false})"
|
|
);
|
|
REQUIRE(tools.front().annotations.has_annotations);
|
|
REQUIRE(tools.front().annotations.read_only_hint);
|
|
REQUIRE(!tools.front().annotations.destructive_hint);
|
|
REQUIRE(tools.front().annotations.idempotent_hint);
|
|
REQUIRE(!tools.front().annotations.open_world_hint);
|
|
|
|
state->authority.store(authority_mode::stale);
|
|
REQUIRE(adapter->advertised_tools(deadline()).empty());
|
|
state->authority.store(authority_mode::unavailable);
|
|
REQUIRE(adapter->advertised_tools(deadline()).empty());
|
|
state->authority.store(authority_mode::mismatch);
|
|
REQUIRE(adapter->advertised_tools(deadline()).empty());
|
|
state->authority.store(authority_mode::fresh);
|
|
REQUIRE(adapter->advertised_tools(deadline()).size() == 1U);
|
|
|
|
state->authority.store(authority_mode::disconnected);
|
|
REQUIRE(adapter->advertised_tools(deadline()).size() == 1U);
|
|
const auto disconnected = adapter->invoke("wallet_status", "{}", deadline());
|
|
REQUIRE(disconnected.status == tool_call_status::ok);
|
|
REQUIRE(
|
|
disconnected.structured_json ==
|
|
R"({"schema_version":1,"connected":false,"fresh_at_ms":9000,"wallet_server_alias":"synthetic-wallet-host","allowed_chain_ids":[8453],"available_actions":["status"],"mutating_actions":[]})"
|
|
);
|
|
REQUIRE(disconnected.content == "Wallet is not connected.");
|
|
state->authority.store(authority_mode::fresh);
|
|
|
|
const auto calls_before_invalid_authority_arguments = state->calls.load();
|
|
for (const auto mode : {
|
|
authority_mode::stale,
|
|
authority_mode::unavailable,
|
|
authority_mode::mismatch,
|
|
}) {
|
|
state->authority.store(mode);
|
|
const auto result = adapter->invoke("wallet_status", R"({"sign":true})", deadline());
|
|
REQUIRE(result.status == tool_call_status::tool_not_found);
|
|
REQUIRE(result.error_message == "tool not found");
|
|
}
|
|
REQUIRE(state->calls.load() == calls_before_invalid_authority_arguments);
|
|
state->authority.store(authority_mode::fresh);
|
|
|
|
const auto calls_before_unknown_tools = state->calls.load();
|
|
for (const std::string_view name : {
|
|
"sage_status",
|
|
"sage_search",
|
|
"sage_execute",
|
|
"glove_control",
|
|
"wallet_sign",
|
|
"wallet_sign_typed_data",
|
|
"wallet_send_transaction",
|
|
"wallet_authenticate",
|
|
"wallet_transfer",
|
|
"wallet_approve",
|
|
"wallet_provider",
|
|
"wallet_p2p",
|
|
}) {
|
|
const auto result = adapter->invoke(name, "{}", deadline());
|
|
REQUIRE(result.status == tool_call_status::tool_not_found);
|
|
REQUIRE(result.error_message == "tool not found");
|
|
}
|
|
REQUIRE(state->calls.load() == calls_before_unknown_tools);
|
|
|
|
const auto calls_before_invalid_arguments = state->calls.load();
|
|
for (const std::string_view arguments : {
|
|
"",
|
|
"null",
|
|
"[]",
|
|
"{",
|
|
R"({"sign":true})",
|
|
R"({"x":1,"x":2})",
|
|
}) {
|
|
const auto result = adapter->invoke("wallet_status", arguments, deadline());
|
|
REQUIRE(result.status == tool_call_status::tool_not_found);
|
|
REQUIRE(result.error_message == "tool not found");
|
|
}
|
|
REQUIRE(state->calls.load() == calls_before_invalid_arguments);
|
|
{
|
|
const std::scoped_lock lock{state->mutex};
|
|
REQUIRE(state->requests.size() == static_cast<std::size_t>(calls_before_invalid_arguments));
|
|
}
|
|
|
|
auto success = adapter->invoke("wallet_status", " { } ", deadline());
|
|
REQUIRE(success.status == tool_call_status::ok);
|
|
REQUIRE(success.content == "Wallet status is available.");
|
|
REQUIRE(
|
|
success.structured_json ==
|
|
R"({"schema_version":1,"connected":true,"fresh_at_ms":9000,"wallet_server_alias":"synthetic-wallet-host","allowed_chain_ids":[8453],"available_actions":["status"],"mutating_actions":[]})"
|
|
);
|
|
REQUIRE(success.error_message.empty());
|
|
const auto private_session = session();
|
|
const std::vector<std::string_view> prohibited_values{
|
|
"private-session-id",
|
|
private_session.controller_plan_digest,
|
|
private_session.plan_content_digest,
|
|
"private-wallet-status-policy",
|
|
"private_key",
|
|
"secret",
|
|
"signature",
|
|
"challenge",
|
|
"rpc",
|
|
"socket",
|
|
"peer",
|
|
"provider",
|
|
"p2p",
|
|
"transaction",
|
|
"typed_data",
|
|
"auth",
|
|
};
|
|
for (const auto prohibited : prohibited_values) {
|
|
REQUIRE(success.structured_json.find(prohibited) == std::string::npos);
|
|
REQUIRE(success.content.find(prohibited) == std::string::npos);
|
|
}
|
|
|
|
state->authority.store(authority_mode::unavailable);
|
|
auto disappeared = adapter->invoke("wallet_status", "{}", deadline());
|
|
REQUIRE(disappeared.status == tool_call_status::tool_not_found);
|
|
REQUIRE(disappeared.error_message == "tool not found");
|
|
REQUIRE(disappeared.content.empty());
|
|
REQUIRE(disappeared.structured_json.empty());
|
|
state->authority.store(authority_mode::fresh);
|
|
|
|
for (const auto mode : {
|
|
channel_mode::malformed,
|
|
channel_mode::wrong_id,
|
|
channel_mode::mutation,
|
|
channel_mode::oversized,
|
|
channel_mode::transport_error,
|
|
channel_mode::throws,
|
|
}) {
|
|
state->channel.store(mode);
|
|
REQUIRE(adapter->advertised_tools(deadline()).empty());
|
|
const auto result = adapter->invoke("wallet_status", "{}", deadline());
|
|
REQUIRE(result.status == tool_call_status::tool_not_found);
|
|
REQUIRE(result.error_message.empty() || result.error_message == "tool not found");
|
|
REQUIRE(result.error_message.find("private") == std::string::npos);
|
|
REQUIRE(result.content.empty());
|
|
REQUIRE(result.structured_json.empty());
|
|
}
|
|
state->channel.store(channel_mode::delegate);
|
|
|
|
std::stop_source cancelled;
|
|
cancelled.request_stop();
|
|
const auto calls_before_cancel = state->calls.load();
|
|
REQUIRE(adapter->advertised_tools(deadline(), cancelled.get_token()).empty());
|
|
REQUIRE(
|
|
adapter->invoke("wallet_status", "{}", deadline(), cancelled.get_token()).status ==
|
|
tool_call_status::tool_not_found
|
|
);
|
|
REQUIRE(state->calls.load() == calls_before_cancel);
|
|
REQUIRE(adapter->advertised_tools(std::chrono::steady_clock::now()).empty());
|
|
REQUIRE(
|
|
adapter->invoke("wallet_status", "{}", std::chrono::steady_clock::now()).status ==
|
|
tool_call_status::tool_not_found
|
|
);
|
|
REQUIRE(
|
|
adapter
|
|
->invoke(
|
|
"wallet_status",
|
|
R"({"sign":true})",
|
|
std::chrono::steady_clock::now(),
|
|
cancelled.get_token()
|
|
)
|
|
.status == tool_call_status::tool_not_found
|
|
);
|
|
REQUIRE(state->calls.load() == calls_before_cancel);
|
|
|
|
state->delay_ms.store(100);
|
|
std::atomic<bool> holder_succeeded{false};
|
|
std::thread holder{[&adapter, &holder_succeeded] {
|
|
holder_succeeded.store(adapter->advertised_tools(deadline()).size() == 1U);
|
|
}};
|
|
const auto holder_wait_deadline =
|
|
std::chrono::steady_clock::now() + std::chrono::milliseconds{200};
|
|
while (state->active.load() != 1 && std::chrono::steady_clock::now() < holder_wait_deadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{1});
|
|
}
|
|
const auto holder_entered = state->active.load() == 1;
|
|
if (!holder_entered) {
|
|
holder.join();
|
|
REQUIRE(holder_entered);
|
|
}
|
|
const auto calls_with_holder = state->calls.load();
|
|
const auto invalid_invoke_started = std::chrono::steady_clock::now();
|
|
const auto invalid_while_locked =
|
|
adapter->invoke("wallet_status", R"({"sign":true})", deadline());
|
|
REQUIRE(invalid_while_locked.status == tool_call_status::tool_not_found);
|
|
REQUIRE(invalid_while_locked.error_message == "tool not found");
|
|
REQUIRE(
|
|
std::chrono::steady_clock::now() - invalid_invoke_started < std::chrono::milliseconds{80}
|
|
);
|
|
REQUIRE(state->calls.load() == calls_with_holder);
|
|
|
|
const auto queued_started = std::chrono::steady_clock::now();
|
|
REQUIRE(adapter->advertised_tools(queued_started + std::chrono::milliseconds{15}).empty());
|
|
REQUIRE(std::chrono::steady_clock::now() - queued_started < std::chrono::milliseconds{80});
|
|
REQUIRE(state->calls.load() == calls_with_holder);
|
|
|
|
std::stop_source queued_cancel;
|
|
std::thread cancel_waiter{[&queued_cancel] {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{10});
|
|
queued_cancel.request_stop();
|
|
}};
|
|
const auto cancel_wait_started = std::chrono::steady_clock::now();
|
|
REQUIRE(adapter->advertised_tools(deadline(), queued_cancel.get_token()).empty());
|
|
REQUIRE(std::chrono::steady_clock::now() - cancel_wait_started < std::chrono::milliseconds{80});
|
|
REQUIRE(state->calls.load() == calls_with_holder);
|
|
cancel_waiter.join();
|
|
holder.join();
|
|
REQUIRE(holder_succeeded.load());
|
|
|
|
state->delay_ms.store(50);
|
|
std::stop_source exchange_cancel;
|
|
std::thread exchange_canceller{[&exchange_cancel] {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{10});
|
|
exchange_cancel.request_stop();
|
|
}};
|
|
const auto calls_before_exchange_cancel = state->calls.load();
|
|
REQUIRE(adapter->advertised_tools(deadline(), exchange_cancel.get_token()).empty());
|
|
exchange_canceller.join();
|
|
REQUIRE(state->calls.load() == calls_before_exchange_cancel + 1);
|
|
|
|
state->delay_ms.store(30);
|
|
const auto calls_before_near_deadline = state->calls.load();
|
|
REQUIRE(adapter
|
|
->advertised_tools(std::chrono::steady_clock::now() + std::chrono::milliseconds{10})
|
|
.empty());
|
|
REQUIRE(state->calls.load() == calls_before_near_deadline + 1);
|
|
state->delay_ms.store(2);
|
|
|
|
std::vector<std::thread> threads;
|
|
threads.reserve(8U);
|
|
for (int index = 0; index < 8; ++index) {
|
|
threads.emplace_back([&adapter] {
|
|
const auto listed = adapter->advertised_tools(deadline());
|
|
if (listed.size() != 1U) {
|
|
std::terminate();
|
|
}
|
|
});
|
|
}
|
|
for (auto& thread : threads) {
|
|
thread.join();
|
|
}
|
|
REQUIRE(state->maximum_active.load() == 1);
|
|
|
|
{
|
|
const std::scoped_lock lock{state->mutex};
|
|
REQUIRE(!state->requests.empty());
|
|
REQUIRE(state->requests.back().find(R"("method":"wallet_status")") != std::string::npos);
|
|
REQUIRE(state->requests.back().find("sign") == std::string::npos);
|
|
REQUIRE(state->requests.back().size() <= glove::control::max_wallet_status_frame_bytes);
|
|
REQUIRE(state->starts.size() == state->deadlines.size());
|
|
REQUIRE(state->requests.size() == state->starts.size());
|
|
for (std::size_t index = 0; index < state->deadlines.size(); ++index) {
|
|
const auto expected_id =
|
|
std::string{"\"id\":\"pi-status-"} + std::to_string(index + 1U) + "\"";
|
|
REQUIRE(state->requests[index].find(expected_id) != std::string::npos);
|
|
REQUIRE(state->deadlines[index] > state->starts[index]);
|
|
REQUIRE(
|
|
state->deadlines[index] - state->starts[index] <= std::chrono::milliseconds{250}
|
|
);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
return run();
|
|
}
|