diff --git a/elixir-orchestration/test/verisim/consensus/kraft_node_test.exs b/elixir-orchestration/test/verisim/consensus/kraft_node_test.exs index 9476471..2d7276c 100644 --- a/elixir-orchestration/test/verisim/consensus/kraft_node_test.exs +++ b/elixir-orchestration/test/verisim/consensus/kraft_node_test.exs @@ -207,7 +207,7 @@ defmodule VeriSim.Consensus.KRaftNodeTest do # Find which one is leader leader_diag = KRaftNode.diagnostics(leader_id) - {actual_leader, actual_follower, actual_follower_pid} = + {actual_leader, actual_follower, _actual_follower_pid} = if leader_diag.role == :leader do {leader_id, follower_id, follower_pid} else diff --git a/elixir-orchestration/test/verisim/consensus/kraft_recovery_test.exs b/elixir-orchestration/test/verisim/consensus/kraft_recovery_test.exs index f28e581..b618d11 100644 --- a/elixir-orchestration/test/verisim/consensus/kraft_recovery_test.exs +++ b/elixir-orchestration/test/verisim/consensus/kraft_recovery_test.exs @@ -26,7 +26,7 @@ defmodule VeriSim.Consensus.KRaftRecoveryTest do "#{prefix}-#{System.unique_integer([:positive])}" end - defp start_node(node_id, opts \\ []) do + defp start_node(node_id, opts) do {:ok, pid} = KRaftNode.start_link([node_id: node_id] ++ opts) pid end diff --git a/elixir-orchestration/test/verisim/query/vql_bridge_test.exs b/elixir-orchestration/test/verisim/query/vql_bridge_test.exs new file mode 100644 index 0000000..652c052 --- /dev/null +++ b/elixir-orchestration/test/verisim/query/vql_bridge_test.exs @@ -0,0 +1,157 @@ +# SPDX-License-Identifier: MPL-2.0 + +defmodule VeriSim.Query.VQLBridgeTest do + @moduledoc """ + Tests for VeriSim.Query.VQLBridge — the Elixir↔Deno bridge for the + ReScript VQL parser. + + When the external Deno/Node subprocess is unavailable (which is the + common case in CI without those runtimes), the bridge falls back to + a pure-Elixir built-in parser. These tests exercise that fallback + path through the public GenServer API — they're guaranteed to hit + the fallback because no subprocess is started in the test + environment. + + `typecheck/2` has no fallback (it requires the ReScript bidirectional + checker), so it must return `{:error, :type_checker_unavailable}` + when the port is nil. + """ + + use ExUnit.Case, async: false + + alias VeriSim.Query.VQLBridge + + setup do + case GenServer.whereis(VQLBridge) do + nil -> + {:ok, _pid} = VQLBridge.start_link([]) + :ok + + _pid -> + :ok + end + + :ok + end + + describe "parse/2 — built-in fallback" do + test "parses a simple SELECT query" do + assert {:ok, ast} = VQLBridge.parse("SELECT GRAPH FROM HEXAD abc-123") + assert is_map(ast) + assert Map.has_key?(ast, :modalities) + assert Map.has_key?(ast, :source) + end + + test "parses multi-modality SELECT" do + assert {:ok, ast} = VQLBridge.parse("SELECT GRAPH, VECTOR, DOCUMENT FROM HEXAD abc-123") + + assert :graph in ast.modalities + assert :vector in ast.modalities + assert :document in ast.modalities + end + + test "parses SELECT * as :all" do + assert {:ok, ast} = VQLBridge.parse("SELECT * FROM HEXAD abc-123") + assert :all in ast.modalities + end + + test "captures the source UUID from FROM HEXAD clause" do + assert {:ok, ast} = VQLBridge.parse("SELECT GRAPH FROM HEXAD entity-xyz") + assert match?({:octad, "entity-xyz"}, ast.source) + end + + test "non-SELECT input returns {:error, _}" do + assert {:error, _} = VQLBridge.parse("HELLO WORLD") + end + + test "empty string returns {:error, _}" do + assert {:error, _} = VQLBridge.parse("") + end + end + + describe "parse_slipstream/2" do + test "accepts a query without PROOF" do + assert {:ok, _ast} = VQLBridge.parse_slipstream("SELECT GRAPH FROM HEXAD abc-123") + end + + test "rejects a query with PROOF clause" do + assert {:error, msg} = + VQLBridge.parse_slipstream( + "SELECT GRAPH FROM HEXAD abc-123 PROOF EXISTENCE(entity-001)" + ) + + assert is_binary(msg) + assert String.contains?(msg, "PROOF") or String.contains?(msg, "Slipstream") + end + end + + describe "parse_dependent/2" do + test "rejects a query without PROOF clause" do + assert {:error, _msg} = VQLBridge.parse_dependent("SELECT GRAPH FROM HEXAD abc-123") + end + + test "accepts a query with PROOF clause" do + assert {:ok, ast} = + VQLBridge.parse_dependent( + "SELECT GRAPH FROM HEXAD abc-123 PROOF EXISTENCE(entity-001)" + ) + + assert ast.proof != nil + end + end + + describe "parse_mutation/2" do + test "parses INSERT statement" do + assert {:ok, mutation} = + VQLBridge.parse_mutation("INSERT HEXAD WITH GRAPH {} AND VECTOR {}") + + assert is_map(mutation) + end + + test "parses UPDATE statement" do + assert {:ok, mutation} = VQLBridge.parse_mutation("UPDATE HEXAD abc-123 SET GRAPH {}") + + assert is_map(mutation) + end + + test "parses DELETE statement" do + assert {:ok, mutation} = VQLBridge.parse_mutation("DELETE HEXAD abc-123") + assert is_map(mutation) + end + + test "non-mutation input returns {:error, _}" do + assert {:error, _} = VQLBridge.parse_mutation("SELECT GRAPH FROM HEXAD x") + end + end + + describe "parse_statement/2" do + test "routes SELECT to Query tag" do + assert {:ok, %{TAG: "Query"}} = VQLBridge.parse_statement("SELECT GRAPH FROM HEXAD abc-123") + end + + test "routes INSERT to Mutation tag" do + assert {:ok, %{TAG: "Mutation"}} = + VQLBridge.parse_statement("INSERT HEXAD WITH GRAPH {} AND VECTOR {}") + end + + test "routes DELETE to Mutation tag" do + assert {:ok, %{TAG: "Mutation"}} = VQLBridge.parse_statement("DELETE HEXAD abc-123") + end + end + + describe "typecheck/2" do + test "returns :type_checker_unavailable when no ReScript subprocess" do + # In test env the Deno/Node subprocess isn't started, so the + # typecheck handler returns this specific error rather than + # silently passing. Critical for VQL-DT integrity. + ast = %{modalities: [:graph], source: {:octad, "abc"}} + assert {:error, :type_checker_unavailable} = VQLBridge.typecheck(ast) + end + end + + describe "parse_and_execute/2 — error propagation" do + test "parse failure propagates" do + assert {:error, _} = VQLBridge.parse_and_execute("INVALID GIBBERISH", []) + end + end +end diff --git a/elixir-orchestration/test/verisim/query/vql_crossmodal_test.exs b/elixir-orchestration/test/verisim/query/vql_crossmodal_test.exs index bdd393d..9ed4b55 100644 --- a/elixir-orchestration/test/verisim/query/vql_crossmodal_test.exs +++ b/elixir-orchestration/test/verisim/query/vql_crossmodal_test.exs @@ -33,7 +33,6 @@ defmodule VeriSim.Query.VQLCrossModalTest do use ExUnit.Case, async: false - alias VeriSim.Query.VQLBridge alias VeriSim.Test.VQLTestHelpers, as: H setup_all do diff --git a/elixir-orchestration/test/verisim/query/vql_e2e_test.exs b/elixir-orchestration/test/verisim/query/vql_e2e_test.exs index ebfef63..f08eb44 100644 --- a/elixir-orchestration/test/verisim/query/vql_e2e_test.exs +++ b/elixir-orchestration/test/verisim/query/vql_e2e_test.exs @@ -28,7 +28,7 @@ defmodule VeriSim.Query.VQLE2ETest do use ExUnit.Case, async: false - alias VeriSim.Query.{VQLBridge, VQLExecutor, VQLTypeChecker, VQLProofCertificate} + alias VeriSim.Query.{VQLTypeChecker, VQLProofCertificate} alias VeriSim.Test.VQLTestHelpers, as: H setup_all do diff --git a/elixir-orchestration/test/verisim/query/vql_executor_test.exs b/elixir-orchestration/test/verisim/query/vql_executor_test.exs new file mode 100644 index 0000000..ae8e7d4 --- /dev/null +++ b/elixir-orchestration/test/verisim/query/vql_executor_test.exs @@ -0,0 +1,222 @@ +# SPDX-License-Identifier: MPL-2.0 + +defmodule VeriSim.Query.VQLExecutorTest do + @moduledoc """ + Tests for VeriSim.Query.VQLExecutor — the 1812-LOC central executor + for parsed VQL queries and mutations. + + The executor sits between the parser (VQLBridge) and the Rust core + (via RustClient). In test environments the Rust core is unreachable, + so most real execution paths bottom out in `{:error, ...}` tuples. + These tests cover what's testable independently: + + - `execute/2` with `:explain` returns a fully-formed plan from the + local cost estimator (Rust planner falls back cleanly on its absence) + - Each source variant (Octad / Federation / Store / Reflect) maps to + a distinct first-step in the plan + - PROOF specs increase the plan's step count and total cost + - Cross-modal WHERE conditions flip the strategy to `:two_phase` + - `execute_mutation/2` routes Insert/Update/Delete/Unknown correctly + - `execute_statement/2` dispatches by TAG and by `:type` key + - `execute_string/2` propagates parse errors as `{:parse_error, _}` + """ + + use ExUnit.Case, async: false + + alias VeriSim.Query.VQLExecutor + + describe "execute/2 with :explain option" do + test "returns a structured plan with the documented keys" do + ast = %{modalities: [:graph], source: {:octad, "abc-123"}} + assert {:ok, plan} = VQLExecutor.execute(ast, explain: true) + + assert is_map(plan) + assert Map.has_key?(plan, :strategy) + assert Map.has_key?(plan, :steps) + assert Map.has_key?(plan, :total_cost_ms) + assert Map.has_key?(plan, :modalities_queried) + assert Map.has_key?(plan, :has_cross_modal) + assert Map.has_key?(plan, :has_proof) + end + + test "default strategy is :sequential when no cross-modal conditions" do + ast = %{modalities: [:graph], source: {:octad, "abc-123"}} + assert {:ok, plan} = VQLExecutor.execute(ast, explain: true) + assert plan.strategy == :sequential + end + + test "octad source produces an 'Octad lookup' step" do + ast = %{modalities: [:graph], source: {:octad, "entity-xyz"}} + {:ok, plan} = VQLExecutor.execute(ast, explain: true) + + ops = Enum.map(plan.steps, & &1.operation) + assert "Octad lookup" in ops + end + + test "federation source produces a 'Federation fan-out' step" do + ast = %{ + modalities: [:graph], + source: {:federation, "/universities/*", :tolerate} + } + + {:ok, plan} = VQLExecutor.execute(ast, explain: true) + ops = Enum.map(plan.steps, & &1.operation) + assert "Federation fan-out" in ops + end + + test "store source produces a 'Store query' step" do + ast = %{modalities: [:graph], source: {:store, "store-1"}} + {:ok, plan} = VQLExecutor.execute(ast, explain: true) + ops = Enum.map(plan.steps, & &1.operation) + assert "Store query" in ops + end + + test "reflect source produces a REFLECT step" do + ast = %{modalities: [:graph], source: :reflect} + {:ok, plan} = VQLExecutor.execute(ast, explain: true) + ops = Enum.map(plan.steps, & &1.operation) + assert Enum.any?(ops, &String.contains?(&1, "REFLECT")) + end + + test "proof obligations bump the total cost" do + base_ast = %{modalities: [:graph], source: {:octad, "x"}} + proof_ast = Map.put(base_ast, :proof, [%{type: "EXISTENCE", target: "y"}]) + + {:ok, base} = VQLExecutor.execute(base_ast, explain: true) + {:ok, with_proof} = VQLExecutor.execute(proof_ast, explain: true) + + assert with_proof.total_cost_ms > base.total_cost_ms + assert with_proof.has_proof == true + assert base.has_proof == false + end + + test "multiple modalities scale the modality-query cost linearly" do + one = %{modalities: [:graph], source: {:octad, "x"}} + + five = %{ + modalities: [:graph, :vector, :tensor, :document, :semantic], + source: {:octad, "x"} + } + + {:ok, p1} = VQLExecutor.execute(one, explain: true) + {:ok, p5} = VQLExecutor.execute(five, explain: true) + assert p5.total_cost_ms > p1.total_cost_ms + end + + test "total_cost_ms equals the sum of step cost_ms values" do + ast = %{ + modalities: [:graph, :vector], + source: {:octad, "x"} + } + + {:ok, plan} = VQLExecutor.execute(ast, explain: true) + sum = Enum.reduce(plan.steps, 0, fn s, acc -> acc + Map.get(s, :cost_ms, 0) end) + assert plan.total_cost_ms == sum + end + end + + describe "execute_mutation/2 — routing" do + test "unknown mutation tag returns {:error, {:invalid_mutation, _}}" do + assert {:error, {:invalid_mutation, _}} = VQLExecutor.execute_mutation(%{TAG: "Bogus"}) + end + + test "Insert routes to execute_insert (errors when RustClient unreachable)" do + mutation = %{TAG: "Insert", modalities: %{graph: %{}}, proof: nil} + result = VQLExecutor.execute_mutation(mutation) + # In test env RustClient.create_octad/1 returns {:error, _}; the + # executor wraps that as {:insert_failed, _}. Either path is + # acceptable — what we're verifying is that the dispatch reached + # the Insert branch and didn't raise. + assert match?({:ok, _}, result) or match?({:error, _}, result) + end + + test "Update routes to execute_update without raising" do + mutation = %{TAG: "Update", octadId: "abc", sets: %{graph: %{}}, proof: nil} + result = VQLExecutor.execute_mutation(mutation) + assert match?({:ok, _}, result) or match?({:error, _}, result) + end + + test "Delete routes to execute_delete without raising" do + mutation = %{TAG: "Delete", octadId: "abc", proof: nil} + result = VQLExecutor.execute_mutation(mutation) + assert match?({:ok, _}, result) or match?({:error, _}, result) + end + end + + describe "execute_statement/2 — dispatch" do + test "%{TAG: \"Query\", _0: query} dispatches to execute/2 with the inner query" do + stmt = %{ + TAG: "Query", + _0: %{modalities: [:graph], source: {:octad, "x"}} + } + + assert {:ok, _plan} = VQLExecutor.execute_statement(stmt, explain: true) + end + + test "%{TAG: \"Mutation\", _0: mutation} dispatches to execute_mutation" do + stmt = %{TAG: "Mutation", _0: %{TAG: "Bogus"}} + assert {:error, {:invalid_mutation, _}} = VQLExecutor.execute_statement(stmt) + end + + test "%{type: :query, ...} dispatches to execute/2" do + stmt = %{type: :query, modalities: [:graph], source: {:octad, "x"}} + assert {:ok, _plan} = VQLExecutor.execute_statement(stmt, explain: true) + end + + test "%{type: :mutation, mutation: ...} dispatches to execute_mutation" do + stmt = %{type: :mutation, mutation: %{TAG: "Bogus"}} + assert {:error, {:invalid_mutation, _}} = VQLExecutor.execute_statement(stmt) + end + + test "unrecognised statement shape falls through to execute/2" do + # An untyped map is forwarded to execute/2, which interprets it + # as a query AST. With :explain we get back a plan without + # needing the Rust core. + stmt = %{modalities: [:graph], source: {:octad, "x"}} + assert {:ok, _plan} = VQLExecutor.execute_statement(stmt, explain: true) + end + end + + describe "execute_string/2 — parse error propagation" do + test "garbage input returns {:error, {:parse_error, _}}" do + assert {:error, {:parse_error, _}} = VQLExecutor.execute_string("THIS IS NOT VALID VQL", []) + end + + test "empty string returns {:error, {:parse_error, _}}" do + assert {:error, {:parse_error, _}} = VQLExecutor.execute_string("", []) + end + + test "valid query string with :explain returns a plan" do + assert {:ok, plan} = + VQLExecutor.execute_string( + "SELECT GRAPH FROM HEXAD abc-123", + explain: true + ) + + assert is_map(plan) + assert Map.has_key?(plan, :strategy) + end + end + + describe "execute/2 — option handling" do + test ":timeout defaults to 30_000 ms" do + # Indirectly verified: passing no opts shouldn't raise. + ast = %{modalities: [:graph], source: {:octad, "x"}} + assert {:ok, _} = VQLExecutor.execute(ast, explain: true) + end + + test ":explain false (default) attempts real execution" do + ast = %{modalities: [:graph], source: {:octad, "x"}} + # Real execution hits RustClient. Without it available, we + # expect an error — but the call must not raise. + result = VQLExecutor.execute(ast) + assert match?({:ok, _}, result) or match?({:error, _}, result) + end + + test ":explain false explicitly attempts real execution" do + ast = %{modalities: [:graph], source: {:octad, "x"}} + result = VQLExecutor.execute(ast, explain: false) + assert match?({:ok, _}, result) or match?({:error, _}, result) + end + end +end diff --git a/rust-core/verisim-repl/src/linter.rs b/rust-core/verisim-repl/src/linter.rs index 77acdae..4bda148 100644 --- a/rust-core/verisim-repl/src/linter.rs +++ b/rust-core/verisim-repl/src/linter.rs @@ -319,7 +319,7 @@ pub fn lint_query(query: &str) -> Vec { } // Sort by severity (errors first) - diagnostics.sort_by(|a, b| b.severity.cmp(&a.severity)); + diagnostics.sort_by_key(|d| std::cmp::Reverse(d.severity)); diagnostics } diff --git a/rust-core/verisim-semantic/src/lib.rs b/rust-core/verisim-semantic/src/lib.rs index efbe80e..44d56b6 100644 --- a/rust-core/verisim-semantic/src/lib.rs +++ b/rust-core/verisim-semantic/src/lib.rs @@ -356,12 +356,12 @@ impl SemanticStore for InMemorySemanticStore { if let Some(typ) = types.get(type_iri) { for constraint in &typ.constraints { match &constraint.kind { - ConstraintKind::Required(prop) => { - if !annotation.properties.contains_key(prop) { - violations - .push(format!("{}: {}", constraint.name, constraint.message)); - } + ConstraintKind::Required(prop) + if !annotation.properties.contains_key(prop) => + { + violations.push(format!("{}: {}", constraint.name, constraint.message)); } + ConstraintKind::Required(_) => {} ConstraintKind::Pattern { property, regex } => { if let Some(SemanticValue::TypedLiteral { value, .. }) = annotation.properties.get(property)