diff --git a/src/cvc/core/state_exec/intrinsics.cpp b/src/cvc/core/state_exec/intrinsics.cpp index fd09ca15..4f860926 100644 --- a/src/cvc/core/state_exec/intrinsics.cpp +++ b/src/cvc/core/state_exec/intrinsics.cpp @@ -238,6 +238,13 @@ value_t intrinsic_state_data_get(intrinsics_context *ctx, std::span(&d)) return deep_copy(*v); + // A RAW std::string blob on the data channel reads back as the DSL `bytes` type (opaque octets), + // NOT an opaque data_object. That raw representation is what state-data-set stores for a `bytes` + // value, what the `?data` URI channel writes (uri_state.cpp), and what the §13.9 HTTP cache parks + // on a node — so a byte blob stays coherent across the resolver and state_exec (types.h: `bytes` + // is opaque octets backed by std::string, the sanctioned home for HTTP octet-stream bodies). + if (const std::string *s = boost::any_cast(&d)) + return make_bytes(*s); auto obj = std::make_shared(); obj->payload = d; obj->type_name = d.type().name(); @@ -258,8 +265,15 @@ value_t intrinsic_state_data_set(intrinsics_context *ctx, std::span(args[1].v)) throw std::runtime_error( "state-data-set: cannot store a callable (function/closure/generator) as data"); - // Store the value_t as boost::any - (*ctx->root)(path).data(boost::any(args[1])); + // A `bytes` value (opaque octets) is stored as a RAW std::string on the data channel — the SAME + // representation the `?data` URI channel (uri_state.cpp) and the §13.9 HTTP cache use — so a byte + // blob written from the DSL is readable via state://…?data and a blob parked by the cache reads + // back as `bytes` here (state-data-get, above). Every other value_t is stored as-is, so + // structured data (lists/dicts/scalars/text strings) round-trips via the value_t path. + if (const bytes_value *b = std::get_if(&args[1].v)) + (*ctx->root)(path).data(boost::any(b->data)); + else + (*ctx->root)(path).data(boost::any(args[1])); return nil_value; } diff --git a/src/cvc/tests/state_exec_intrinsics_test.cpp b/src/cvc/tests/state_exec_intrinsics_test.cpp index 2ed87f09..af878e03 100644 --- a/src/cvc/tests/state_exec_intrinsics_test.cpp +++ b/src/cvc/tests/state_exec_intrinsics_test.cpp @@ -1,6 +1,7 @@ /// @file state_exec_intrinsics_test.cpp /// @brief Tests for Phase 5: Intrinsics, Resource Policy, Stdlib. +#include #include #include #include @@ -253,6 +254,51 @@ TEST_F(StateTreeIntrinsicsTest, StateDataGetNonexistent) { EXPECT_TRUE(result.is_nil()); } +TEST_F(StateTreeIntrinsicsTest, StateDataGetReadsRawStringBlobAsBytes) { + // A RAW std::string parked on the data channel — what the ?data URI channel (uri_state.cpp) and + // the §13.9 HTTP cache write — reads back as the DSL `bytes` type, NOT an opaque data_object. + // This is the coherence the cache depends on: a byte blob is the same on both sides. + cvc::state &root = cvc::state::instance(app_ctx); + root("blob").data(boost::any(std::string("raw-octets"))); + auto got = call("state-data-get", {std::string("blob")}); + ASSERT_TRUE(std::holds_alternative(got.v)); + EXPECT_EQ(std::get(got.v).data, "raw-octets"); +} + +TEST_F(StateTreeIntrinsicsTest, StateDataSetBytesStoredAsRawStringForDataChannel) { + // A `bytes` value set from the DSL is stored as a RAW std::string on the data channel (the ?data + // / cache representation), so it round-trips as `bytes` AND is byte-serializable via + // state://…?data. + const std::string octets("A\0B\0C", 5); // embedded NULs prove byte transparency + call("state-data-set", {std::string("b"), make_bytes(octets)}); + + auto got = call("state-data-get", {std::string("b")}); // round-trips as bytes, byte-exact + ASSERT_TRUE(std::holds_alternative(got.v)); + EXPECT_EQ(std::get(got.v).data, octets); + + // the on-node payload is a raw std::string — exactly what uri_state.cpp's ?data any_cast reads + cvc::state *n = cvc::state::instance(app_ctx).findDescendant("b"); + ASSERT_NE(n, nullptr); + const boost::any d = n->data(); + const std::string *raw = boost::any_cast(&d); + ASSERT_NE(raw, nullptr) << "a bytes value must land as a raw std::string for ?data compatibility"; + EXPECT_EQ(*raw, octets); +} + +TEST_F(StateTreeIntrinsicsTest, StateDataStringAndBytesTracksAreDistinct) { + // The text `string` track and the `bytes` track stay distinct through state-data-set/get: a text + // string comes back a string (stored as a value_t), a bytes value comes back bytes. + call("state-data-set", {std::string("t"), value_t(std::string("text"))}); + auto t = call("state-data-get", {std::string("t")}); + ASSERT_TRUE(std::holds_alternative(t.v)); // string track (unchanged) + EXPECT_EQ(std::get(t.v), "text"); + + call("state-data-set", {std::string("y"), make_bytes(std::string("bin"))}); + auto y = call("state-data-get", {std::string("y")}); + ASSERT_TRUE(std::holds_alternative(y.v)); // bytes track + EXPECT_EQ(std::get(y.v).data, "bin"); +} + TEST_F(StateTreeIntrinsicsTest, StateGetWrongArgCount) { EXPECT_THROW(call("state-get", {}), std::runtime_error); EXPECT_THROW(call("state-get", {std::string("a"), std::string("b")}), std::runtime_error);