test/openagents_web/install_script_test.exs

58e6347eeb72 · 9 KB

defmodule OpenAgentsWeb.InstallScriptTest do
  @moduledoc """
  The installer is a published entry point, so what it refuses matters.

  `curl -fsSL https://openagents.com/install.sh | bash` hands a shell script
  the right to write an executable onto the reader's machine and put it on
  their PATH. Two properties keep that honest: the script has to actually be
  served under that name, and it has to refuse anything it cannot verify.

  The refusals are asserted against the script's own text rather than by
  running it, because running it means reaching the network. `bash -n` proves
  it parses; the behaviour these assert -- no unverified install, no
  unnamed source -- is proven end to end against a fixture release server in
  the issue's record.
  """

  use ExUnit.Case, async: true

  @script Path.join([File.cwd!(), "priv", "static", "install.sh"])

  defp libc(source, arch, root) do
    {output, 0} =
      System.cmd("bash", ["-c", ". #{source}; linux_libc #{arch} #{root}"],
        stderr_to_stdout: true
      )

    String.trim(output)
  end

  defp fixture(dir, name, paths) do
    root = Path.join(dir, name)
    File.mkdir_p!(root)

    Enum.each(paths, fn path ->
      absolute = Path.join(root, path)
      File.mkdir_p!(Path.dirname(absolute))
      File.write!(absolute, "")
    end)

    root
  end

  test "the installer is served under the name the published command uses" do
    assert "install.sh" in OpenAgentsWeb.static_paths(),
           "`install.sh` is not in static_paths/0, so /install.sh is a 404"

    assert Enum.any?(OpenAgentsWeb.static_prefixes(), &String.starts_with?("install.sh", &1)),
           "no prefix in static_prefixes/0 covers install.sh, so it 404s once digested"
  end

  test "the script parses" do
    assert {_output, 0} = System.cmd("bash", ["-n", @script], stderr_to_stdout: true)
  end

  test "the script needs no shell Alpine does not ship" do
    # The musl builds exist for Alpine above all, and Alpine ships no bash.
    # `curl … | bash` there fails before the first line runs, with an error
    # about the shell rather than about anything the reader did. So this is
    # POSIX shell, and these are the three things that would quietly make it
    # bash again.
    assert {_output, 0} = System.cmd("sh", ["-n", @script], stderr_to_stdout: true)

    script = File.read!(@script)

    body =
      script
      |> String.replace(~r/^#.*$/m, "")
      # `[[:space:]]` and its siblings are POSIX character classes. They are
      # valid in `sh` and in POSIX `grep -E`, busybox included, and they
      # contain `[[` without being a bash conditional. Drop them before
      # looking for one, or the check below fails on correct POSIX code.
      |> String.replace(~r/\[\[:[a-z]+:\]\]/, "")

    refute body =~ "[[", "`[[ ]]` is a bash conditional"
    refute body =~ "=~", "`=~` is a bash regex match"
    refute body =~ ~r/\[@\]/, "an array expansion is bash-only"
  end

  test "nothing is installed without a checksum that matches" do
    script = File.read!(@script)

    assert script =~ "SHA256SUMS-",
           "the installer does not fetch a sums file"

    assert script =~ "refusing to install unverified bytes",
           "the installer does not refuse when the sums file is missing"

    assert script =~ "Checksum mismatch",
           "the installer does not refuse a mismatched artifact"
  end

  test "there is no path that installs a binary nobody named" do
    script = File.read!(@script)

    # The removed fallback copied ./target/release/oa, ./target/debug/oa, or an
    # already-installed binary when the download failed, so running the
    # installer anywhere near a stale build installed that build while
    # reporting the version it meant to fetch.
    refute script =~ ~r/^\s*cp\s+"?\.\/target\//m,
           "the installer copies a local build when the download fails"

    refute script =~ ~r/^\s*cp\s+"\$HOME\/\.openagents\/bin/m,
           "the installer reinstalls whatever is already on disk"
  end

  describe "libc detection" do
    # `linux_libc` is extracted from the served script and run against fixture
    # roots, so these assert the code readers actually receive rather than a
    # copy of it. The choice matters more than most: a glibc binary on a musl
    # system fails at exec with "no such file or directory" naming a file that
    # is plainly there, and nothing in that message points at the installer.
    setup do
      script = File.read!(@script)

      [function] = Regex.run(~r/^linux_libc\(\).*?^\}/ms, script)

      dir = Path.join(System.tmp_dir!(), "install-libc-#{System.unique_integer([:positive])}")
      File.mkdir_p!(dir)
      on_exit(fn -> File.rm_rf(dir) end)

      source = Path.join(dir, "linux_libc.sh")
      File.write!(source, function)

      {:ok, source: source, dir: dir}
    end

    test "a present glibc loader means the dynamically linked build runs", context do
      root = fixture(context.dir, "glibc", ["lib64/ld-linux-x86-64.so.2"])

      assert libc(context.source, "x86_64", root) == "gnu"
    end

    test "a multiarch glibc loader counts too", context do
      root = fixture(context.dir, "multiarch", ["lib/x86_64-linux-gnu/ld-linux-x86-64.so.2"])

      assert libc(context.source, "x86_64", root) == "gnu"
    end

    test "no glibc loader means only the static build runs", context do
      root = fixture(context.dir, "alpine", ["lib/ld-musl-x86_64.so.1"])

      assert libc(context.source, "x86_64", root) == "musl"
    end

    test "an image carrying no libc at all still resolves to the static build", context do
      root = fixture(context.dir, "bare", [])

      assert libc(context.source, "x86_64", root) == "musl"
      assert libc(context.source, "aarch64", root) == "musl"
    end

    test "a glibc host with the musl package installed is still glibc", context do
      # Debian's `musl` package drops a musl loader onto a glibc system. The
      # question is which artifact runs, not which loaders exist, and both do.
      root =
        fixture(context.dir, "both", [
          "lib64/ld-linux-x86-64.so.2",
          "lib/ld-musl-x86_64.so.1"
        ])

      assert libc(context.source, "x86_64", root) == "gnu"
    end

    test "each architecture is judged by its own loader", context do
      root = fixture(context.dir, "arm", ["lib/ld-linux-aarch64.so.1"])

      assert libc(context.source, "aarch64", root) == "gnu"

      # The x86_64 loader is absent from this root, and an architecture's
      # verdict must not be borrowed from another's.
      assert libc(context.source, "x86_64", root) == "musl"
    end

    test "an architecture with no musl build asks for the glibc one", context do
      root = fixture(context.dir, "riscv", [])

      assert libc(context.source, "riscv64", root) == "gnu"
    end
  end

  test "the musl platform is named the way the release publishes it" do
    script = File.read!(@script)

    assert script =~ ~s(platform="${platform}-musl"),
           "the installer detects musl and then asks for the same artifact anyway"

    refute script =~ ~s(platform="${os}-${arch}-${libc}"),
           "the glibc artifact is renamed, which breaks every installer already in circulation"
  end

  test "the channel resolves a version rather than hardcoding one" do
    script = File.read!(@script)

    refute script =~ ~r/^\s*version="0\.1\.0"/m,
           "the installer still hardcodes a version, so the channel decides nothing"

    assert script =~ "${BASE_URL_PRIMARY}/${CHANNEL}",
           "the installer never reads the channel pointer"
  end

  describe "the name it installs" do
    # This reverses the `oa`-only rule. That rule existed because the Rust
    # binary answered a strict subset of the CLI's commands, so taking the
    # `openagents` name swapped a fraction of the CLI in underneath every
    # script that called it. That is no longer true: the shipped binary
    # dispatches the whole command set and is the coder session besides.
    test "installs openagents, coder, and oa for the same binary" do
      script = File.read!(@script)

      assert script =~ ~s(ln -sf "$link_target" "$BIN_DIR/openagents"),
             "the installer does not create the openagents name"

      assert script =~ ~s(ln -sf "$link_target" "$BIN_DIR/coder"),
             "the installer does not create the coder name"

      assert script =~ ~s(ln -sf "$link_target" "$BIN_DIR/oa"),
             "the installer does not create the oa alias"
    end

    test "starts Coder from the installed path after it finishes" do
      script = File.read!(@script)

      assert script =~ ~s(OpenAgents v$version installed.)
      assert script =~ ~s(Loading Coder...)

      assert script =~ ~s(exec "$coder_path"),
             "the installer does not start Coder after installation"

      refute script =~ ~s(exec "$coder_path" </dev/tty),
             "redirecting stdin breaks Coder's macOS input reader"

      # The closing block used to carry a shadow warning, a PATH comparison and
      # a note about a different CLI. It was noise on a successful install.
      refute script =~ "TypeScript CLI",
             "the installer talks about a different CLI"

      refute script =~ "npm i -g",
             "the installer sends the reader somewhere else"

      refute script =~ "installs 'oa' only",
             "the installer still claims it installs oa only"
    end
  end
end