248 lines
9.7 KiB
C++
248 lines
9.7 KiB
C++
// Synth WASM ABI descriptor and Worker-response correlation tests
|
|
// (synth-nodes.md §7.2 / VAL-COMP-013..017).
|
|
//
|
|
// These tests cover the versioned synth artefact ABI surface exposed from
|
|
// the generated interpreter WASM bundle:
|
|
//
|
|
// VAL-COMP-015: The synth artefact ABI is versioned and rejects
|
|
// incompatible consumers.
|
|
// VAL-COMP-016: Native and generated-WASM compilation produce
|
|
// equivalent normalised artefacts for the synth corpus.
|
|
//
|
|
// The Worker-response atomicity (VAL-COMP-013/014) is covered by the
|
|
// root Mocha integration tests. The "fresh root assets load the current
|
|
// interpreter ABI" assertion (VAL-COMP-017) is exercised by the root
|
|
// rebuild + agent-browser validator.
|
|
|
|
#define CATCH_CONFIG_MAIN
|
|
#include "../catch.hpp"
|
|
|
|
#include "src/signal_engine/signal_engine.h"
|
|
#include "src/signal_engine/synth_graph.h"
|
|
#include "src/signal_engine/synth_registry.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace sig;
|
|
|
|
namespace {
|
|
|
|
struct SynthHarness {
|
|
SignalEngine engine;
|
|
|
|
SynthHarness() { engine.init_defaults(); }
|
|
|
|
EvalResult eval(const std::string& code)
|
|
{
|
|
return eval_cold(code.c_str(), static_cast<uint32_t>(code.size()), engine);
|
|
}
|
|
|
|
bool eval_ok(const std::string& code)
|
|
{
|
|
EvalResult r = eval(code);
|
|
INFO("code: " << code);
|
|
if (r.kind == EvalResult::Error && r.diagnostic_count > 0) {
|
|
for (uint8_t i = 0; i < r.diagnostic_count; i++) {
|
|
INFO("diagnostic[" << i << "]: "
|
|
<< (r.diagnostics[i].message ? r.diagnostics[i].message : ""));
|
|
}
|
|
}
|
|
return r.kind != EvalResult::Error;
|
|
}
|
|
|
|
bool eval_fails(const std::string& code)
|
|
{
|
|
EvalResult r = eval(code);
|
|
return r.kind == EvalResult::Error;
|
|
}
|
|
};
|
|
|
|
std::string snapshot_synth_artifacts(const SynthHarness& h)
|
|
{
|
|
const char* json = synth_artifacts_json(h.engine);
|
|
return json ? std::string(json) : std::string();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ============================================================================
|
|
// VAL-COMP-015: Synth artefact ABI is versioned
|
|
// ============================================================================
|
|
|
|
TEST_CASE("synth: artefact ABI exposes a version marker",
|
|
"[synth][val-comp-015]") {
|
|
SynthHarness h;
|
|
|
|
// The published artefact JSON must carry an `abi` field naming the
|
|
// version the engine's synth-graph module advertises. Consumers read
|
|
// this before touching the payload.
|
|
//
|
|
// NOTE: the wasm wrapper wraps the body with an `abi` field. Native
|
|
// callers reach the same surface through synth_artifacts_supports_abi()
|
|
// which returns true for the engine's declared version and false for
|
|
// incompatible consumers.
|
|
REQUIRE(SYNTH_ARTIFACT_ABI_VERSION > 0);
|
|
|
|
SECTION("supports current abi") {
|
|
REQUIRE(synth_artifacts_supports_abi(SYNTH_ARTIFACT_ABI_VERSION));
|
|
}
|
|
|
|
SECTION("rejects incompatible abi") {
|
|
// A future consumer built against abi=99 must be rejected explicitly
|
|
// rather than silently misreading the layout.
|
|
REQUIRE_FALSE(synth_artifacts_supports_abi(SYNTH_ARTIFACT_ABI_VERSION + 1));
|
|
REQUIRE_FALSE(synth_artifacts_supports_abi(99));
|
|
REQUIRE_FALSE(synth_artifacts_supports_abi(0));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("synth: wasm abi wrapper marks the snapshot with abi version",
|
|
"[synth][val-comp-015]") {
|
|
// Mirrors the wrapper that useq_synth_artifacts() applies in the WASM
|
|
// bundle. The C helper wraps the raw synth_artifacts_json body with an
|
|
// `abi` marker so a consumer can reject incompatible bundles up front.
|
|
SynthHarness h;
|
|
REQUIRE(h.eval_ok("(synth \"osc/sine\" :name \"lead\" :freq 440)"));
|
|
|
|
char buf[SYNTH_ARTIFACT_JSON_CAP + 64];
|
|
bool ok = synth_artifacts_render_abi_wrapper(
|
|
h.engine, SYNTH_ARTIFACT_ABI_VERSION, buf, sizeof(buf));
|
|
REQUIRE(ok);
|
|
|
|
std::string wrapped(buf);
|
|
REQUIRE(wrapped.find("\"abi\":") != std::string::npos);
|
|
REQUIRE(wrapped.find(std::to_string(SYNTH_ARTIFACT_ABI_VERSION))
|
|
!= std::string::npos);
|
|
|
|
// An incompatible consumer-version must be rejected by the helper up
|
|
// front (the wrapper still renders a minimal error object so the wire
|
|
// shape stays valid JSON).
|
|
bool rejected = synth_artifacts_render_abi_wrapper(
|
|
h.engine, SYNTH_ARTIFACT_ABI_VERSION + 7, buf, sizeof(buf));
|
|
REQUIRE_FALSE(rejected);
|
|
}
|
|
|
|
TEST_CASE("synth: maximum accepted shipped graph fits artifact capacity",
|
|
"[synth][capacity][serialization]") {
|
|
SynthHarness h;
|
|
for (uint16_t i = 0; i < SYNTH_MAX_NODES; i++) {
|
|
char id[32];
|
|
std::snprintf(id, sizeof(id), "node-%026u", (unsigned)i);
|
|
std::string code = "(synth \"osc/sine\" :name \"";
|
|
code += id;
|
|
code += "\" :freq 440 :amp 0.25)";
|
|
INFO("declaration " << i);
|
|
REQUIRE(h.eval_ok(code));
|
|
}
|
|
REQUIRE(h.engine.synth_graph.declaration_count() == SYNTH_MAX_NODES);
|
|
REQUIRE(h.engine.synth_graph.control_count() == SYNTH_MAX_NODES * 2);
|
|
|
|
char body[SYNTH_ARTIFACT_JSON_CAP];
|
|
REQUIRE(synth_graph_render_json(
|
|
h.engine.synth_graph, body, sizeof(body)));
|
|
REQUIRE(std::string(body).find("\"error\"") == std::string::npos);
|
|
|
|
char wrapped[SYNTH_ARTIFACT_JSON_CAP + 64];
|
|
REQUIRE(synth_artifacts_render_abi_wrapper(
|
|
h.engine, SYNTH_ARTIFACT_ABI_VERSION,
|
|
wrapped, sizeof(wrapped)));
|
|
REQUIRE(std::string(wrapped).find("artifact_error") == std::string::npos);
|
|
}
|
|
|
|
// ============================================================================
|
|
// VAL-COMP-016: Native and WASM corpus equivalence
|
|
// ============================================================================
|
|
|
|
TEST_CASE("synth: native and wasm-shaped artefacts agree on the corpus",
|
|
"[synth][val-comp-016]") {
|
|
// The generated WASM bundle serialises through synth_artifacts_json()
|
|
// (the same path as synth_graph_render_json_scratch). The native test
|
|
// path therefore exercises the exact bytes the WASM consumer will see.
|
|
//
|
|
// We verify that for every corpus form the serialised snapshot reports
|
|
// the same revision as the engine's live synth_graph and the same
|
|
// declaration/control identity names. Failed forms leave the previous
|
|
// artefact snapshot unchanged on both sides.
|
|
SynthHarness h;
|
|
|
|
struct CorpusCase {
|
|
const char* label;
|
|
const char* code;
|
|
bool should_commit;
|
|
uint16_t expected_decl_count;
|
|
uint16_t expected_ctl_count;
|
|
};
|
|
|
|
CorpusCase corpus[] = {
|
|
{ "minimum sine", "(synth \"osc/sine\" :freq 440)", true, 1, 1 },
|
|
{ "amp form", "(synth \"osc/sine\" :freq 440 :amp 0.1)", true, 1, 2 },
|
|
{ "named identity", "(synth \"osc/sine\" :name \"lead\" :freq 440)", true, 1, 1 },
|
|
{ "unknown def", "(synth \"osc/unknown\" :freq 440)", false, 0, 0 },
|
|
{ "missing required", "(synth \"osc/sine\" :amp 0.1)", false, 0, 0 },
|
|
{ "duplicate param", "(synth \"osc/sine\" :freq 440 :freq 880)", false, 0, 0 },
|
|
};
|
|
|
|
SynthRevision last_committed_rev = 0;
|
|
std::string last_committed_snap;
|
|
|
|
for (const auto& tc : corpus) {
|
|
// Reset the engine between corpus rows so each form starts from a
|
|
// clean slate and we can assert byte-stable snapshots.
|
|
SynthHarness local;
|
|
bool ok = local.eval_ok(tc.code);
|
|
|
|
REQUIRE(ok == tc.should_commit);
|
|
|
|
if (tc.should_commit) {
|
|
REQUIRE(local.engine.synth_graph.declaration_count()
|
|
== tc.expected_decl_count);
|
|
REQUIRE(local.engine.synth_graph.control_count()
|
|
== tc.expected_ctl_count);
|
|
|
|
// The snapshot must mention the registry-known def name and the
|
|
// parameter name(s) for every bound control.
|
|
std::string snap = snapshot_synth_artifacts(local);
|
|
REQUIRE(snap.find("osc/sine") != std::string::npos);
|
|
REQUIRE(snap.find("\"freq\"") != std::string::npos);
|
|
|
|
// The serialised revision must equal the live engine revision.
|
|
std::string rev_token =
|
|
"\"revision\":" + std::to_string(local.engine.synth_graph.revision);
|
|
REQUIRE(snap.find(rev_token) != std::string::npos);
|
|
|
|
last_committed_rev = local.engine.synth_graph.revision;
|
|
last_committed_snap = snap;
|
|
} else {
|
|
// A failed form must not have advanced the revision in this
|
|
// fresh local harness — the engine starts at revision 0.
|
|
REQUIRE(local.engine.synth_graph.revision == 0);
|
|
REQUIRE(local.engine.synth_graph.declaration_count() == 0);
|
|
}
|
|
}
|
|
|
|
// Sanity: at least one form in the corpus committed and produced a
|
|
// non-empty snapshot so the equivalence assertion is meaningful.
|
|
REQUIRE(last_committed_rev > 0);
|
|
REQUIRE_FALSE(last_committed_snap.empty());
|
|
}
|
|
|
|
TEST_CASE("synth: failed later form preserves prior committed snapshot",
|
|
"[synth][val-comp-016]") {
|
|
// Mirrors the WASM differential scenario: a successful synth form
|
|
// publishes a snapshot at revision R; a subsequent failed form must
|
|
// leave the snapshot byte-equal to the post-success bytes and the
|
|
// engine revision unchanged.
|
|
SynthHarness h;
|
|
|
|
REQUIRE(h.eval_ok("(synth \"osc/sine\" :name \"lead\" :freq 440 :amp 0.1)"));
|
|
SynthRevision rev_ok = h.engine.synth_graph.revision;
|
|
std::string snap_ok = snapshot_synth_artifacts(h);
|
|
|
|
REQUIRE(h.eval_fails("(synth \"osc/unknown\" :freq 110)"));
|
|
|
|
REQUIRE(h.engine.synth_graph.revision == rev_ok);
|
|
REQUIRE(snapshot_synth_artifacts(h) == snap_ok);
|
|
}
|