// 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 #include #include #include 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(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); }