mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-30 21:41:10 +00:00
* fix(codec): signedness and range are part of the integer type Codec<T>::from accepted any integral JSON number and handed it to .get<T>(). That is silent in both directions: .get<uint64_t>() on -1 -> 18446744073709551615 (a sign flip) .get<int32_t>() on 2^40 -> truncated Both now reject with the usual path-carrying CodecError instead. Rejecting is the codec's existing contract — a value the declared type cannot represent must not reach business logic wearing a different one — this just extends it to the half of the integer domain it was skipping. Note the check is on the JSON category, not the value: a negative literal parses as number_integer and never as number_unsigned, so `is_number_unsigned()` is the reliable discriminator rather than a comparison after conversion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(async): the pending-call sentinel is matched by shape, not by key presence All four detection sites tested `m.contains(pendingCallKey())` and nothing else, so ANY user map carrying that key was taken for a deferred call: the consumer extracted a call id, found no completion, and waited out a nested event loop. The measured outcome is a ~20s HANG, not a fast failure. An `any` slot is enough to reach it — anything a user can put in a map. logos::isPendingCallSentinel now requires the canonical shape: exactly one entry, under the sentinel key, holding a non-empty string. Shape and signature are mirrored from isUnauthorizedSentinel (logos_rpc_status.h), QJsonObject arm included — the two are the same kind of in-band marker and there was no reason for them to be guarded differently. That guard, and isTaggedBytes's, both already existed in this repo; the difference was chronology, not principle. Behaviour-preserving: the generated glue builds this map with exactly one entry whose value is a QString call id, so no real sender changes. The concurrent dispatch tests pass unchanged. NARROWS, DOES NOT CLOSE — and the tests say so out loud. A one-key, string-valued forgery IS the sentinel; no predicate can separate them. It still hangs, and because call ids are a per-object counter from 0, a forged "lc-0" can collide with a genuine in-flight completion and steal its result. Closing that needs an out-of-band channel for "deferred", which the single-QVariant dispatch slot cannot express without an ABI break — the constraint is stated at logos_rpc_status.h:24-27 and is real. tests: 10 new, including one asserting the forgery still matches, so a future reader cannot mistake the green cells for "the sentinel is safe". 236/236. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
133 lines
4.4 KiB
C++
133 lines
4.4 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <QJsonObject>
|
|
#include <QString>
|
|
#include <QVariant>
|
|
#include <QVariantList>
|
|
#include <QVariantMap>
|
|
|
|
#include "logos_async_dispatch.h"
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The pending-call sentinel is in-band: a "multi" provider returns a QVariantMap
|
|
// carrying the call id under __logos_pending_call__, because the dispatch slot
|
|
// returns a single QVariant and there is no out-of-band place to say "deferred"
|
|
// without an ABI break.
|
|
//
|
|
// In-band means user data can imitate it. The four detection sites used to test
|
|
// `m.contains(pendingCallKey())` and nothing else, so ANY map carrying that key
|
|
// was taken for a deferred call: the consumer waited in a nested event loop for
|
|
// a completion that never arrives and the call hung for the full timeout. An
|
|
// `any` slot is enough to reach it.
|
|
//
|
|
// These pin the shape check. They do NOT claim the sentinel is unforgeable — a
|
|
// forgery of the canonical shape is still indistinguishable, because that is the
|
|
// real thing. See known.json.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
namespace {
|
|
|
|
QVariant canonical(const QString& id)
|
|
{
|
|
QVariantMap m;
|
|
m[logos::pendingCallKey()] = id;
|
|
return QVariant(m);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST(PendingSentinel, CanonicalShapeIsRecognisedAndYieldsTheCallId)
|
|
{
|
|
QString id;
|
|
ASSERT_TRUE(logos::isPendingCallSentinel(canonical("lc-7"), &id));
|
|
EXPECT_EQ(id, QStringLiteral("lc-7"));
|
|
}
|
|
|
|
TEST(PendingSentinel, CallIdOutParamIsOptional)
|
|
{
|
|
EXPECT_TRUE(logos::isPendingCallSentinel(canonical("lc-0")));
|
|
}
|
|
|
|
// The case the conformance matrix measured: user data that merely CONTAINS the
|
|
// key. Previously hijacked the call; must now pass through as an ordinary map.
|
|
TEST(PendingSentinel, ExtraKeysMeanItIsOrdinaryUserData)
|
|
{
|
|
QVariantMap m;
|
|
m[logos::pendingCallKey()] = QStringLiteral("lc-1");
|
|
m[QStringLiteral("x")] = 2;
|
|
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(QVariant(m)));
|
|
}
|
|
|
|
// echoAny({"__logos_pending_call__": 1, "x": 2}) — the exact matrix payload.
|
|
TEST(PendingSentinel, MatrixPayloadIsNotASentinel)
|
|
{
|
|
QVariantMap m;
|
|
m[logos::pendingCallKey()] = 1;
|
|
m[QStringLiteral("x")] = 2;
|
|
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(QVariant(m)));
|
|
}
|
|
|
|
// A call id is a string. A one-key map whose value is a number is user data.
|
|
TEST(PendingSentinel, NonStringValueIsNotASentinel)
|
|
{
|
|
QVariantMap m;
|
|
m[logos::pendingCallKey()] = 1;
|
|
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(QVariant(m)));
|
|
}
|
|
|
|
TEST(PendingSentinel, EmptyCallIdIsNotASentinel)
|
|
{
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(canonical(QString())));
|
|
}
|
|
|
|
TEST(PendingSentinel, WrongKeyIsNotASentinel)
|
|
{
|
|
QVariantMap m;
|
|
m[QStringLiteral("pending")] = QStringLiteral("lc-1");
|
|
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(QVariant(m)));
|
|
}
|
|
|
|
TEST(PendingSentinel, NonMapValuesAreNotSentinels)
|
|
{
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(QVariant()));
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(QVariant(QStringLiteral("lc-1"))));
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(QVariant(42)));
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(QVariant(QVariantList{1, 2})));
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(QVariant(QVariantMap{})));
|
|
}
|
|
|
|
// The QJsonObject arm mirrors isUnauthorizedSentinel's: some json_convert paths
|
|
// historically produced QJsonObject rather than QVariantMap.
|
|
TEST(PendingSentinel, QJsonObjectArmBehavesTheSame)
|
|
{
|
|
QJsonObject good;
|
|
good[logos::pendingCallKey()] = QStringLiteral("lc-2");
|
|
QString id;
|
|
ASSERT_TRUE(logos::isPendingCallSentinel(QVariant(good), &id));
|
|
EXPECT_EQ(id, QStringLiteral("lc-2"));
|
|
|
|
QJsonObject extra;
|
|
extra[logos::pendingCallKey()] = QStringLiteral("lc-2");
|
|
extra[QStringLiteral("x")] = 2;
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(QVariant(extra)));
|
|
|
|
QJsonObject numeric;
|
|
numeric[logos::pendingCallKey()] = 1;
|
|
EXPECT_FALSE(logos::isPendingCallSentinel(QVariant(numeric)));
|
|
}
|
|
|
|
// What the guard does NOT do, pinned so nobody reads the green cells above as
|
|
// "the sentinel is safe". A canonical-shape forgery is the real thing.
|
|
TEST(PendingSentinel, CanonicalShapeForgeryIsStillIndistinguishable)
|
|
{
|
|
QVariantMap forged;
|
|
forged[logos::pendingCallKey()] = QStringLiteral("lc-0");
|
|
|
|
EXPECT_TRUE(logos::isPendingCallSentinel(QVariant(forged)))
|
|
<< "if this ever fails the in-band design changed — update known.json";
|
|
}
|