|
| 1 | +defmodule ExPhil.Eval.PortCheckTest do |
| 2 | + @moduledoc """ |
| 3 | + Pins the dummy-port decision (GOTCHAS #57 / #57b) — the check that gates |
| 4 | + every CPU-dummy eval block and had no tests until 2026-08-03, despite the |
| 5 | + bug it guards having silently invalidated the entire combo-drill era of |
| 6 | + recordings (five of six on 2026-07-26 came up HUMAN). |
| 7 | + """ |
| 8 | + use ExUnit.Case, async: true |
| 9 | + |
| 10 | + alias ExPhil.Eval.PortCheck |
| 11 | + |
| 12 | + defp cpu(port, level), do: %{"port" => port, "type" => 1, "cpu_level" => level} |
| 13 | + defp human(port), do: %{"port" => port, "type" => 0, "cpu_level" => nil} |
| 14 | + |
| 15 | + describe "verify/2" do |
| 16 | + test "passes when the requested port is a CPU at the requested level" do |
| 17 | + assert PortCheck.verify([human(1), cpu(2, 9)], expect_cpu: 2, expect_level: 9) == :ok |
| 18 | + end |
| 19 | + |
| 20 | + test "catches THE bug: a HUMAN port where a CPU was requested" do |
| 21 | + assert {:error, :not_cpu, msg} = |
| 22 | + PortCheck.verify([human(1), human(2)], expect_cpu: 2) |
| 23 | + |
| 24 | + assert msg =~ "NOT a CPU" |
| 25 | + assert msg =~ "#57", "the message must name the gotcha so the reader finds the cause" |
| 26 | + end |
| 27 | + |
| 28 | + test "catches the autostart race: CPU present but at Melee's default level 1" do |
| 29 | + assert {:error, :wrong_level, msg} = |
| 30 | + PortCheck.verify([human(1), cpu(2, 1)], expect_cpu: 2, expect_level: 9) |
| 31 | + |
| 32 | + assert msg =~ "level 1" |
| 33 | + assert msg =~ "expected 9" |
| 34 | + end |
| 35 | + |
| 36 | + test "catches an absent port" do |
| 37 | + assert {:error, :absent, _} = PortCheck.verify([human(1)], expect_cpu: 2) |
| 38 | + end |
| 39 | + |
| 40 | + test "tolerates a build that does not report cpu_level" do |
| 41 | + # Level is only checked when the replay reports one — some builds |
| 42 | + # return nil, and failing there would reject good recordings. |
| 43 | + assert PortCheck.verify([human(1), cpu(2, nil)], expect_cpu: 2, expect_level: 9) == :ok |
| 44 | + end |
| 45 | + |
| 46 | + test "no expectation means no check (the default eval path)" do |
| 47 | + assert PortCheck.verify([human(1), human(2)]) == :ok |
| 48 | + end |
| 49 | + end |
| 50 | + |
| 51 | + describe "type_name/1" do |
| 52 | + test "names every Slippi port type" do |
| 53 | + assert PortCheck.type_name(0) == "HUMAN" |
| 54 | + assert PortCheck.type_name(1) == "CPU" |
| 55 | + assert PortCheck.type_name(2) == "DEMO" |
| 56 | + assert PortCheck.type_name(3) == "empty" |
| 57 | + assert PortCheck.type_name(nil) =~ "type=" |
| 58 | + end |
| 59 | + end |
| 60 | +end |
| 61 | + |
| 62 | +defmodule ExPhil.Test.AnalogReleaseEdgeTest do |
| 63 | + @moduledoc """ |
| 64 | + GOTCHAS #66: an equivalence check is only as strong as its tolerance |
| 65 | + classes. The scenario drift check treated the shield family 178-182 as |
| 66 | + equivalent, so a broken analog RELEASE (EXI inputs latch neutral) scored |
| 67 | + "15/15 exact" while P2 rode shield to break and dizzy every stock. |
| 68 | +
|
| 69 | + `ReplicationCheck` had the same blindness in a different place — it |
| 70 | + compared the digital l/r bits and ignored the analog shoulder axis |
| 71 | + entirely. This pins that a release EDGE is now visible to `:exact`. |
| 72 | + """ |
| 73 | + use ExUnit.Case, async: true |
| 74 | + |
| 75 | + alias ExPhil.Bridge.ControllerState |
| 76 | + alias ExPhil.Test.ReplicationCheck |
| 77 | + |
| 78 | + defp ctrl(shoulder) do |
| 79 | + %ControllerState{ |
| 80 | + main_stick: %{x: 0.5, y: 0.5}, |
| 81 | + c_stick: %{x: 0.5, y: 0.5}, |
| 82 | + l_shoulder: shoulder, |
| 83 | + r_shoulder: 0.0, |
| 84 | + button_a: false, |
| 85 | + button_b: false, |
| 86 | + button_x: false, |
| 87 | + button_y: false, |
| 88 | + button_z: false, |
| 89 | + button_l: false, |
| 90 | + button_r: false, |
| 91 | + button_d_up: false |
| 92 | + } |
| 93 | + end |
| 94 | + |
| 95 | + test "a latched analog release is NOT scored as exact" do |
| 96 | + # expected: press then RELEASE. actual: press then LATCH (never releases) |
| 97 | + expected = [ctrl(1.0), ctrl(1.0), ctrl(0.0)] |
| 98 | + latched = [ctrl(1.0), ctrl(1.0), ctrl(1.0)] |
| 99 | + |
| 100 | + assert {:error, diag} = ReplicationCheck.check(expected, latched, strictness: :exact) |
| 101 | + |
| 102 | + refute diag.pass, |
| 103 | + "a latched analog release must fail :exact — this is the #66 blindness that " <> |
| 104 | + "turned 'release is broken' into 15/15 exact" |
| 105 | + end |
| 106 | + |
| 107 | + test "an identical analog stream still passes" do |
| 108 | + stream = [ctrl(1.0), ctrl(1.0), ctrl(0.0)] |
| 109 | + assert {:ok, diag} = ReplicationCheck.check(stream, stream, strictness: :exact) |
| 110 | + assert diag.pass |
| 111 | + end |
| 112 | +end |
0 commit comments