|
| 1 | +defmodule ExPhil.Harness.BackboneDefaultsTest do |
| 2 | + @moduledoc """ |
| 3 | + Guards the atom contract between three lists that must agree: |
| 4 | +
|
| 5 | + * `Config.@valid_backbones` — the CLI gate |
| 6 | + * `Networks.Policy.Backbone.build_temporal_backbone/3` — the dispatcher |
| 7 | + * `Config.backbone_defaults/1` — per-backbone hyperparameters |
| 8 | +
|
| 9 | + Found 2026-08-03 while preparing the architecture bake-off: |
| 10 | + `backbone_defaults/1` had clauses for `:ret_net` and `:mamba_3` while both |
| 11 | + the dispatcher and the CLI gate spell them `:retnet` and `:mamba3`. Those |
| 12 | + clauses could never match, so those backbones trained with NO defaults — |
| 13 | + no precision pin, no window size, no layer count — silently, because a |
| 14 | + missing default is indistinguishable from "this backbone needs none". |
| 15 | +
|
| 16 | + That matters most exactly when it is least visible: a wide sweep where |
| 17 | + nobody inspects each arm's config. |
| 18 | + """ |
| 19 | + use ExUnit.Case, async: true |
| 20 | + |
| 21 | + alias ExPhil.Training.Config |
| 22 | + |
| 23 | + # Backbones whose defaults are deliberately absent (they take the generic |
| 24 | + # path). Keeping this explicit means "no entry" is a decision, not a typo. |
| 25 | + @intentionally_undefaulted [] |
| 26 | + |
| 27 | + describe "backbone_defaults/1 keys are real backbones" do |
| 28 | + test "every backbone with defaults is CLI-selectable" do |
| 29 | + valid = MapSet.new(Config.valid_backbones()) |
| 30 | + |
| 31 | + defaulted = |
| 32 | + Config.valid_backbones() |
| 33 | + |> Enum.filter(&(Config.backbone_defaults(&1) not in [nil, []])) |
| 34 | + |> MapSet.new() |
| 35 | + |
| 36 | + # Any atom that HAS defaults must be a valid backbone. We can only |
| 37 | + # enumerate via valid_backbones/0, so the real check is the inverse |
| 38 | + # below plus the spot-checks — this asserts the intersection is sane. |
| 39 | + assert MapSet.subset?(defaulted, valid) |
| 40 | + end |
| 41 | + |
| 42 | + test "the two backbones that were silently untuned now get defaults" do |
| 43 | + for backbone <- [:retnet, :mamba3] do |
| 44 | + defaults = Config.backbone_defaults(backbone) |
| 45 | + |
| 46 | + assert defaults not in [nil, []], |
| 47 | + "#{inspect(backbone)} has no defaults — this is the :ret_net/:mamba_3 " <> |
| 48 | + "atom-typo class: the clause exists but under a name the dispatcher " <> |
| 49 | + "never uses, so the backbone trains untuned." |
| 50 | + |
| 51 | + assert Keyword.has_key?(defaults, :temporal) |
| 52 | + end |
| 53 | + end |
| 54 | + |
| 55 | + test "the misspelled atoms are gone" do |
| 56 | + for typo <- [:ret_net, :mamba_3] do |
| 57 | + refute typo in Config.valid_backbones(), |
| 58 | + "#{inspect(typo)} is not a real backbone atom" |
| 59 | + |
| 60 | + assert Config.backbone_defaults(typo) in [nil, []], |
| 61 | + "#{inspect(typo)} should have no defaults clause — it is a typo of a real " <> |
| 62 | + "backbone, and a clause under it can never match the dispatcher." |
| 63 | + end |
| 64 | + end |
| 65 | + |
| 66 | + test "undefaulted backbones are an explicit list, not an accident" do |
| 67 | + undefaulted = |
| 68 | + Config.valid_backbones() |
| 69 | + |> Enum.filter(&(Config.backbone_defaults(&1) in [nil, []])) |
| 70 | + |
| 71 | + # This is informational rather than a hard gate: ~23 of the CLI's |
| 72 | + # backbones have tuned defaults today and filling the rest is the |
| 73 | + # bake-off's job. The assertion pins the KNOWN-intentional set so a |
| 74 | + # newly-typo'd key shows up as a diff here. |
| 75 | + assert Enum.all?(@intentionally_undefaulted, &(&1 in undefaulted)) |
| 76 | + end |
| 77 | + end |
| 78 | +end |
0 commit comments