defmodule Mix.Tasks.Verify.Adopter do use Mix.Task @shortdoc "Runs fast adopter contracts, or the live Phoenix example proof with --live" @moduledoc """ Runs the maintainer-facing adopter verification flow from the repository root. Default mode is the fast, auth-free, service-free adopter contract slice: - `mix test test/scrypath/readiness_contract_test.exs` - `mix test test/mix/tasks/verify_adopter_test.exs` Pass `--live` to run the canonical Phoenix example proof path under `examples/phoenix_meilisearch`: - `cd examples/phoenix_meilisearch` - `mix deps.get` - `mix test` Live mode requires these environment variables before it will run: - `SCRYPATH_EXAMPLE_INTEGRATION` - `PGPORT` - `SCRYPATH_MEILISEARCH_URL` Live mode also expects the example's Postgres and Meilisearch services to already be running. This task checks those prerequisites, but it still stays orchestration-only. This task stays orchestration-only. It does not start Docker, provision services, or silently downgrade from live mode back to fast mode. For the full maintainer matrix and CI job names, see [CONTRIBUTING.md](CONTRIBUTING.md). """ @fast_tests [ "test/scrypath/readiness_contract_test.exs", "test/mix/tasks/verify_adopter_test.exs" ] @required_live_envs [ "SCRYPATH_EXAMPLE_INTEGRATION", "PGPORT", "SCRYPATH_MEILISEARCH_URL" ] @impl true def run(args) do Mix.Task.run("app.start") {opts, argv, invalid} = OptionParser.parse(args, strict: [fast: :boolean, live: :boolean] ) ensure_valid_args!(opts, argv, invalid) if opts[:live] do run_live!() else run_fast!() end end defp run_fast! do Mix.shell().info("==> verify.adopter: running fast adopter contracts") run_test!(@fast_tests, "fast adopter contracts") end defp run_live! do example_dir = Path.expand("examples/phoenix_meilisearch", File.cwd!()) unless File.dir?(example_dir) do Mix.raise("verify.adopter: expected #{example_dir} to exist") end ensure_live_env!() ensure_live_services!() Mix.shell().info( "==> verify.adopter: cd examples/phoenix_meilisearch && mix deps.get && mix test" ) script = "printf 'n\\n' | mix deps.get && mix test" {out, status} = System.cmd("bash", ["-lc", script], cd: example_dir, stderr_to_stdout: true) Mix.shell().info(out) if status != 0 do Mix.raise("verify.adopter failed: `#{script}` (in #{example_dir}) exited #{status}") end end defp run_test!(args, label) do Mix.shell().info("==> Running #{label}") Mix.Task.reenable("test") Mix.Task.run("test", args) end defp ensure_valid_args!(opts, [], []) do if opts[:fast] && opts[:live] do Mix.raise("verify.adopter accepts either --fast or --live, not both") end end defp ensure_valid_args!(_opts, argv, invalid) do invalid_flags = Enum.map(invalid, fn {name, _value} -> format_invalid_flag(name) name when is_atom(name) -> "--#{name}" other -> format_invalid_flag(other) end) tokens = argv ++ invalid_flags Mix.raise("verify.adopter does not accept arguments, got: #{Enum.join(tokens, " ")}") end defp ensure_live_env! do missing = Enum.filter(@required_live_envs, fn name -> System.get_env(name) in [nil, ""] end) if missing != [] do Mix.raise(""" verify.adopter --live requires environment variables: #{Enum.join(missing, ", ")} Example: SCRYPATH_EXAMPLE_INTEGRATION=1 PGPORT=5433 SCRYPATH_MEILISEARCH_URL=http://127.0.0.1:7700 mix verify.adopter --live """) end end defp ensure_live_services! do postgres_port = postgres_port!() {meilisearch_host, meilisearch_port} = meilisearch_endpoint!() unreachable = [ {"Postgres", "localhost", postgres_port}, {"Meilisearch", meilisearch_host, meilisearch_port} ] |> Enum.reject(fn {_label, host, port} -> reachable?(host, port) end) if unreachable != [] do failures = Enum.map_join(unreachable, ", ", fn {label, host, port} -> "#{label} on #{host}:#{port}" end) Mix.raise(""" verify.adopter --live requires running Postgres and Meilisearch services before it shells into the example app. Unreachable: #{failures} Start the example stack first: cd examples/phoenix_meilisearch docker compose up -d Then rerun: SCRYPATH_EXAMPLE_INTEGRATION=1 PGPORT=5433 SCRYPATH_MEILISEARCH_URL=http://127.0.0.1:7700 mix verify.adopter --live """) end end defp postgres_port! do "PGPORT" |> System.fetch_env!() |> String.to_integer() rescue ArgumentError -> Mix.raise("verify.adopter --live requires PGPORT to be an integer TCP port") end defp meilisearch_endpoint! do url = System.fetch_env!("SCRYPATH_MEILISEARCH_URL") case URI.parse(url) do %URI{host: host, scheme: scheme, port: port} when is_binary(host) and host != "" -> {host, port || URI.default_port(scheme) || 80} _ -> Mix.raise( "verify.adopter --live requires SCRYPATH_MEILISEARCH_URL to include a valid host, got: #{inspect(url)}" ) end end defp reachable?(host, port) do case :gen_tcp.connect(String.to_charlist(host), port, [:binary, active: false], 1_000) do {:ok, socket} -> :gen_tcp.close(socket) true {:error, _reason} -> false end end defp format_invalid_flag(<<"--", _::binary>> = value), do: value defp format_invalid_flag(value), do: "--#{value}" end