diff --git a/ffi/connectors.json b/ffi/connectors.json new file mode 100644 index 00000000..7347d71a --- /dev/null +++ b/ffi/connectors.json @@ -0,0 +1,22 @@ +{ + "_spdx": "MPL-2.0", + "_note": "Golden source-of-truth for the hexadeca-connector wire contract. The Zig enum (ffi/zig/src/hexadeca.zig), the Idris2 ABI (src/abi/Types.idr), and the Rust client (clients/rust/hypatia-client/src/connector.rs) MUST all agree with this list, in this exact order; id is the C ABI wire id. Drift is guarded by test/hexadeca_contract_test.exs. Wire ordering is load-bearing -- do not renumber.", + "connectors": [ + { "id": 0, "name": "grpc" }, + { "id": 1, "name": "graphql" }, + { "id": 2, "name": "rest" }, + { "id": 3, "name": "flatbuffers" }, + { "id": 4, "name": "bebop" }, + { "id": 5, "name": "jsonrpc" }, + { "id": 6, "name": "websocket" }, + { "id": 7, "name": "mqtt" }, + { "id": 8, "name": "trpc" }, + { "id": 9, "name": "capnproto" }, + { "id": 10, "name": "soap" }, + { "id": 11, "name": "verisimdb-rest" }, + { "id": 12, "name": "bsp" }, + { "id": 13, "name": "scip" }, + { "id": 14, "name": "ipfs" }, + { "id": 15, "name": "arrow-flight" } + ] +} diff --git a/test/hexadeca_contract_test.exs b/test/hexadeca_contract_test.exs new file mode 100644 index 00000000..b45b1905 --- /dev/null +++ b/test/hexadeca_contract_test.exs @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: MPL-2.0 +defmodule Hypatia.HexadecaContractTest do + @moduledoc false + use ExUnit.Case, async: true + + # Single-oracle drift guard for the hexadeca-connector wire contract. + # + # `ffi/connectors.json` is the golden source-of-truth for the 16-connector + # surface. The Zig enum, the Idris2 ABI, and the Rust client each mirror it; + # this test reads the golden plus all three source files and fails if any + # mirror drifts in name or order. Wire ordering is load-bearing + # (see ffi/zig/src/hexadeca.zig). + + @root Path.expand("..", __DIR__) + @golden Path.join(@root, "ffi/connectors.json") + + @sources [ + {"Zig (hexadeca.zig)", Path.join(@root, "ffi/zig/src/hexadeca.zig"), + ~r/\.\w+\s*=>\s*"([a-z0-9-]+)"/}, + {"Idris2 (Types.idr)", Path.join(@root, "src/abi/Types.idr"), + ~r/connectorName\s+\w+\s*=\s*"([a-z0-9-]+)"/}, + {"Rust (connector.rs)", Path.join(@root, "clients/rust/hypatia-client/src/connector.rs"), + ~r/Connector::\w+\s*=>\s*"([a-z0-9-]+)"/} + ] + + defp golden_connectors do + @golden |> File.read!() |> Jason.decode!() |> Map.fetch!("connectors") + end + + defp names_in(path, regex) do + regex |> Regex.scan(File.read!(path)) |> Enum.map(fn [_, name] -> name end) + end + + test "golden fixture lists the 16 connectors with sequential wire ids" do + conns = golden_connectors() + assert length(conns) == 16 + assert Enum.map(conns, & &1["id"]) == Enum.to_list(0..15) + end + + test "every language mirror matches the golden in name and order" do + golden_names = Enum.map(golden_connectors(), & &1["name"]) + + for {label, path, regex} <- @sources do + names = names_in(path, regex) + + assert length(names) == 16, + "#{label}: expected 16 connector names, found #{length(names)}" + + assert names == golden_names, + "#{label} drifted from ffi/connectors.json\n golden: #{inspect(golden_names)}\n found: #{inspect(names)}" + end + end +end