From f1d92910b4b0834ce3cf18f5737059f5c769aa6a Mon Sep 17 00:00:00 2001 From: Guarzo Date: Sat, 1 Aug 2026 20:18:17 +0000 Subject: [PATCH 01/10] fix(db): repair maps_v1.scopes charlist default Migrations 20260331192521 and 20260406213852 declared the column default as `default: '{wormholes}'`. A single-quoted Elixir literal is a charlist, so the generated DDL stored an eleven-element text array holding the character codes of the literal string "{wormholes}" instead of the one-element array `{wormholes}`. Any row inserted without an explicit `scopes` value got that garbage, and every later read of the row failed with Ash.Error.Unknown ("cannot load ... as type {:array, Ash.Type.Atom}"). This is what made SlugRecoveryTest's automatic recovery test fail: its raw-SQL fixture rows omit `scopes`, so re-reading the map after recovery blew up and MapRepo.get_map_by_slug_safely/2 reported :unknown_error. The error had nothing to do with duplicate-slug handling. Adds a corrective migration that sets the proper default and repairs rows already written with the bad value, and fixes `migration_defaults` in the Map resource so regenerated migrations no longer reintroduce the charlist. --- lib/wanderer_app/api/map.ex | 5 ++- ...20260801200000_fix_maps_scopes_default.exs | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 priv/repo/migrations/20260801200000_fix_maps_scopes_default.exs diff --git a/lib/wanderer_app/api/map.ex b/lib/wanderer_app/api/map.ex index c71c8efc..05bdd9c6 100644 --- a/lib/wanderer_app/api/map.ex +++ b/lib/wanderer_app/api/map.ex @@ -14,7 +14,10 @@ defmodule WandererApp.Api.Map do repo(WandererApp.Repo) table("maps_v1") - migration_defaults scopes: "'{wormholes}'" + # This value is injected verbatim into generated migrations. It must be + # Elixir source for a list of strings: `'{wormholes}'` is a charlist, which + # generated a default of the character codes of the literal "{wormholes}". + migration_defaults scopes: ~s(["wormholes"]) end json_api do diff --git a/priv/repo/migrations/20260801200000_fix_maps_scopes_default.exs b/priv/repo/migrations/20260801200000_fix_maps_scopes_default.exs new file mode 100644 index 00000000..ddcf686b --- /dev/null +++ b/priv/repo/migrations/20260801200000_fix_maps_scopes_default.exs @@ -0,0 +1,40 @@ +defmodule WandererApp.Repo.Migrations.FixMapsScopesDefault do + @moduledoc """ + Repairs the `maps_v1.scopes` column default. + + Migrations 20260331192521 and 20260406213852 declared the default as + `default: '{wormholes}'`. In Elixir a single-quoted literal is a charlist, + i.e. `[123, 119, 111, ...]`, so Ecto emitted an eleven-element text array of + the character codes of the literal string `{wormholes}` instead of the + one-element array `{wormholes}`. + + Any row inserted without an explicit `scopes` value therefore received + garbage that cannot be cast back to `{:array, :atom}`, and every subsequent + read of that row failed with `Ash.Error.Unknown` ("cannot load ... as type"). + + This migration sets the correct default and repairs rows already written with + the bad value. + """ + + use Ecto.Migration + + # Character codes of the literal string "{wormholes}", which is what the + # charlist default expanded to. + @bad_default ~w(123 119 111 114 109 104 111 108 101 115 125) + + def up do + alter table(:maps_v1) do + modify :scopes, {:array, :text}, default: ["wormholes"] + end + + execute( + "UPDATE maps_v1 SET scopes = ARRAY['wormholes']::text[] WHERE scopes = ARRAY[#{Enum.map_join(@bad_default, ",", &"'#{&1}'")}]::text[]" + ) + end + + def down do + alter table(:maps_v1) do + modify :scopes, {:array, :text}, default: nil + end + end +end From ed686ee8423bfd92492b4071e17409f2b4f91e5e Mon Sep 17 00:00:00 2001 From: Guarzo Date: Sat, 1 Aug 2026 20:18:17 +0000 Subject: [PATCH 02/10] test(auth): give AuthController tests a session and flash The SSO callback failure path calls put_flash/3, which requires the session and flash plugs installed by the :browser pipeline. These tests built a bare build_conn/0, so all six died with "flash not fetched, call fetch_flash/2" before reaching the redirect they assert on. This is a test setup defect, not a controller defect -- the controller legitimately expects the standard browser pipeline. Fixed by routing the affected cases through a browser_conn/0 helper that initialises a test session and fetches flash, rather than weakening the controller or the assertions. --- .../unit/controllers/auth_controller_test.exs | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/test/unit/controllers/auth_controller_test.exs b/test/unit/controllers/auth_controller_test.exs index 0cfb73fe..94f668e6 100644 --- a/test/unit/controllers/auth_controller_test.exs +++ b/test/unit/controllers/auth_controller_test.exs @@ -5,7 +5,7 @@ defmodule WandererAppWeb.AuthControllerTest do describe "parameter validation and error handling" do test "callback/2 validates missing assigns" do - conn = build_conn() + conn = browser_conn() params = %{} # Should handle gracefully when required assigns are missing @@ -34,7 +34,7 @@ defmodule WandererAppWeb.AuthControllerTest do test "callback/2 handles malformed auth data gracefully" do # Test with minimal conn structure to exercise error paths # The callback/2 function will match the fallback clause and redirect - conn = build_conn() + conn = browser_conn() result = AuthController.callback(conn, %{}) @@ -46,7 +46,7 @@ defmodule WandererAppWeb.AuthControllerTest do test "callback/2 processes auth structure with missing fields" do # Test the fallback clause since auth structure is incomplete # Missing CharacterOwnerHash will cause pattern match failure - conn = build_conn() + conn = browser_conn() result = AuthController.callback(conn, %{}) @@ -58,7 +58,7 @@ defmodule WandererAppWeb.AuthControllerTest do test "callback/2 exercises character creation path" do # Test the fallback clause for now since character creation involves complex validation # The actual implementation requires valid EVE character data which is complex to mock - conn = build_conn() + conn = browser_conn() result = AuthController.callback(conn, %{}) @@ -69,7 +69,7 @@ defmodule WandererAppWeb.AuthControllerTest do test "callback/2 handles existing user assignment" do # Test the fallback clause for consistent behavior - conn = build_conn() + conn = browser_conn() result = AuthController.callback(conn, %{}) @@ -81,8 +81,8 @@ defmodule WandererAppWeb.AuthControllerTest do test "callback/2 validates various auth credential formats" do # Test fallback clause behavior for various cases test_cases = [ - build_conn(), - build_conn() |> assign(:some_other_assign, "value") + browser_conn(), + browser_conn() |> assign(:some_other_assign, "value") ] Enum.each(test_cases, fn conn -> @@ -192,4 +192,15 @@ defmodule WandererAppWeb.AuthControllerTest do end) end end + + # The SSO callback failure path calls put_flash/3, which requires the session + # and flash plugs that the real :browser pipeline installs. A bare + # build_conn/0 has neither, so these tests died with "flash not fetched" + # before ever reaching the redirect they assert on. This mirrors the browser + # pipeline rather than relaxing the controller. + defp browser_conn do + build_conn() + |> Plug.Test.init_test_session(%{}) + |> Phoenix.ConnTest.fetch_flash() + end end From cd3fc662ddcb3c85a7396679acf13bf001543704 Mon Sep 17 00:00:00 2001 From: Guarzo Date: Sat, 1 Aug 2026 20:49:00 +0000 Subject: [PATCH 03/10] test(map): stop MapScopesTest leaking system static info cache MapScopesTest seeds :system_static_info_cache -- a global Cachex cache shared by the whole run -- with stub records carrying only :solar_system_id and :system_class, and never removed them. Contrary to appearances, MapScopesTest was the polluter rather than the victim. System 30_000_142 (Jita) is also used by CommonAPIControllerTest and OpenAPIValidationTest, which insert a full MapSolarSystem record and then read it back through the same cache. Whichever suite ran second lost, so the observed failures moved between MapScopesTest and the API suites depending on --seed. Proven by bisecting to a minimal reproducing pair: mix test test/integration/api/common_api_controller_test.exs \ test/unit/map/map_scopes_test.exs --seed 42 # 1 failure mix test test/integration/api/common_api_controller_test.exs --seed 42 # clean mix test test/unit/map/map_scopes_test.exs --seed 42 # clean Fixed by deleting the seeded keys in on_exit. --- test/unit/map/map_scopes_test.exs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/unit/map/map_scopes_test.exs b/test/unit/map/map_scopes_test.exs index 21f07346..aa929456 100644 --- a/test/unit/map/map_scopes_test.exs +++ b/test/unit/map/map_scopes_test.exs @@ -70,6 +70,19 @@ defmodule WandererApp.Map.Server.MapScopesTest do Cachex.put(:system_static_info_cache, solar_system_id, system_info) end) + # :system_static_info_cache is global and shared with every other suite in + # the run. These stub entries carry only :solar_system_id and :system_class, + # so leaving them behind silently replaces the full records other suites + # expect -- 30_000_142 (Jita) in particular is also used by + # CommonAPIControllerTest and OpenAPIValidationTest, which then read a + # record missing solar_system_name and friends. Whether that surfaced + # depended on suite order, which is why these failures moved with --seed. + on_exit(fn -> + Enum.each(Elixir.Map.keys(test_systems), fn solar_system_id -> + Cachex.del(:system_static_info_cache, solar_system_id) + end) + end) + :ok end From 9357dafa97ab3e598c280eacca4793e3a1d45273 Mon Sep 17 00:00:00 2001 From: Guarzo Date: Sat, 1 Aug 2026 22:13:22 +0000 Subject: [PATCH 04/10] test(map): stop leaking fake stargates into the global jump cache map_scope_filtering_test.exs seeds two fabricated "jump_*" entries into the global WandererApp.Cache to simulate stargates, and never removed them. CachedInfo.get_solar_system_jump/2 only rebuilds the jump index when a key is missing, so once written those entries persist for the rest of the run. They cover systems 30_000_001 / 30_000_002 / 30_000_100 -- exactly the IDs MapScopesTest uses for its "k-space connection is a wormhole when NO stargate exists" cases. Whichever suite ran first decided the answer, which is why the 9 MapScopesTest failures moved around with --seed and vanished in isolation. Proven by instrumenting the assertion rather than inferring from the message: in isolation get_solar_system_jump(30_000_001, 30_000_002) returns {:ok, nil}, but in the full run it returns a jump struct -- while the database itself contains no such row (verified with a direct count against map_solar_system_jumps_v1). Running the same file set with --max-cases 1 was clean, confirming a shared-cache race rather than a plain ordering bug. Fixed by deleting both keys in on_exit. --- test/integration/map/map_scope_filtering_test.exs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/integration/map/map_scope_filtering_test.exs b/test/integration/map/map_scope_filtering_test.exs index 176b885b..9ad35cd6 100644 --- a/test/integration/map/map_scope_filtering_test.exs +++ b/test/integration/map/map_scope_filtering_test.exs @@ -213,6 +213,17 @@ defmodule WandererApp.Map.MapScopeFilteringTest do to_solar_system_id: @ls_system_halmah }) + # These "jump_*" keys live in the global cache and are never invalidated + # once written -- CachedInfo.get_solar_system_jump/2 only rebuilds the index + # when a key is missing. MapScopesTest asserts on the same system IDs + # (30_000_001 / 30_000_002 / 30_000_100) expecting NO stargate, so leaving + # these behind made its "valid when no stargate exists" cases fail depending + # on whether this suite happened to run first. + on_exit(fn -> + WandererApp.Cache.delete(halenan_mili_key) + WandererApp.Cache.delete(halenan_halmah_key) + end) + :ok end From 113a7047e0b0ad1094adf38b20386c2dad32b589 Mon Sep 17 00:00:00 2001 From: Guarzo Date: Sat, 1 Aug 2026 22:14:34 +0000 Subject: [PATCH 05/10] ci: let the test job actually fail, and run integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test workflow was structurally incapable of reporting failure. 1. `mix test --cover 2>&1 || true` discarded the exit status, and the `exit_code=$?` two lines below measured the preceding `echo`, so it was always 0. The step now captures the real status and exits with it. 2. `continue-on-error: true` on the test step meant that even when the workflow's own parsing correctly computed "status=❌ Some Failed", the job still reported green. Removed from the test step only. It is deliberately kept on the genuinely optional reporting steps -- coverage, Credo, Dialyzer, and the summary -- since those are advisory and should not gate a merge. Those steps plus the PR-comment steps gained `if: always()` so the results comment is still posted when the suite is red, which is exactly when it is most useful. 3. Integration tests never ran anywhere in CI: test/test_helper.exs excludes :integration by default and no workflow passed --include. They now run in a separate `integration-test` job, so local `mix test` stays fast. That job is marked continue-on-error because these ~64 tests have no CI history; promoting them to a hard merge gate is a policy decision for maintainers. Verified rather than reasoned about. With a deliberately failing test added locally and the step's shell replicated verbatim: new logic: status=FAILED, test_exit_code=2 -> job fails old logic: exit_code=0 -> job green The temporary test was then removed. --- .github/workflows/test.yml | 81 ++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac9f87ab..6f8fcbf7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,8 +103,12 @@ jobs: - name: Run tests with coverage id: tests run: | - # Run tests with coverage - output=$(mix test --cover 2>&1 || true) + # Run tests with coverage. The exit code is captured rather than + # discarded: this step must fail the job when the suite is red. + set +e + output=$(mix test --cover 2>&1) + test_exit_code=$? + set -e echo "$output" > test_output.txt # Parse test results @@ -133,11 +137,12 @@ jobs: fi echo "success_rate=$success_rate" >> $GITHUB_OUTPUT - exit_code=$? - echo "exit_code=$exit_code" >> $GITHUB_OUTPUT - continue-on-error: true + # Fail the job on a red suite. Reporting steps below still run + # because they depend on this step's outputs via always(). + exit $test_exit_code - name: Generate coverage report + if: always() id: coverage run: | # Generate coverage report with GitHub format @@ -163,6 +168,7 @@ jobs: continue-on-error: true - name: Run Credo analysis + if: always() id: credo run: | # Run Credo and capture output @@ -200,6 +206,7 @@ jobs: continue-on-error: true - name: Run Dialyzer analysis + if: always() id: dialyzer run: | # Ensure PLT is built @@ -227,6 +234,7 @@ jobs: continue-on-error: true - name: Create test results summary + if: always() id: summary run: | # Calculate overall score @@ -254,7 +262,7 @@ jobs: continue-on-error: true - name: Find existing PR comment - if: github.event_name == 'pull_request' + if: always() && github.event_name == 'pull_request' id: find_comment uses: peter-evans/find-comment@v3 with: @@ -263,7 +271,7 @@ jobs: body-includes: '## 🧪 Test Results Summary' - name: Create or update PR comment - if: github.event_name == 'pull_request' + if: always() && github.event_name == 'pull_request' uses: peter-evans/create-or-update-comment@v4 with: comment-id: ${{ steps.find_comment.outputs.comment-id }} @@ -331,3 +339,62 @@ jobs: 🤖 *Auto-generated by GitHub Actions* • Updated: ${{ github.event.head_commit.timestamp }} > **Note**: This comment will be updated automatically when new commits are pushed to this PR. + + # Integration tests are excluded from the default `mix test` run by + # test/test_helper.exs, so until now they had never executed in CI at all. + # They run here as a separate job rather than by changing the default + # exclusion, so local `mix test` stays fast. + # + # continue-on-error is deliberate and temporary: these ~64 tests have no CI + # history, so gating merges on them from day one is a policy change for + # maintainers to make, not this PR. Remove it to turn them into a hard gate. + integration-test: + name: Integration Tests (non-blocking) + runs-on: ubuntu-latest + continue-on-error: true + + services: + postgres: + image: postgres:15 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: wanderer_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Elixir/OTP + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.ELIXIR_VERSION }} + otp-version: ${{ env.OTP_VERSION }} + + - name: Cache Elixir dependencies + uses: actions/cache@v3 + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix- + + - name: Install Elixir dependencies + run: | + mix deps.get + mix deps.compile + + - name: Setup database + run: | + mix ecto.create + mix ecto.migrate + + - name: Run integration tests + run: mix test --include integration From 5a09522ef2942f949e370db795e62a3d46407d52 Mon Sep 17 00:00:00 2001 From: Guarzo Date: Sat, 1 Aug 2026 23:54:51 +0000 Subject: [PATCH 06/10] refactor: address review feedback on the scopes migration - Derive the bad-default constant from the offending literal (~c"{wormholes}") instead of hand-transcribing its character codes. A typo in the hardcoded list would have made the WHERE clause match nothing and silently repaired zero rows. Verified the generated SQL is byte-identical. - Document why down/0 is deliberately not a mirror of up/0: restoring the original charlist default would reintroduce the bug, so it drops the default instead and leaves repaired rows repaired. - Note that both 20260331192521 and 20260406213852 used exactly this literal, so the single value catches rows written by either (verified by grep). - Use Map.keys/1 rather than an unnecessary Elixir.Map prefix -- Map is not aliased in that test module. --- .../20260801200000_fix_maps_scopes_default.exs | 12 +++++++++--- test/unit/map/map_scopes_test.exs | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/priv/repo/migrations/20260801200000_fix_maps_scopes_default.exs b/priv/repo/migrations/20260801200000_fix_maps_scopes_default.exs index ddcf686b..f7b93dbb 100644 --- a/priv/repo/migrations/20260801200000_fix_maps_scopes_default.exs +++ b/priv/repo/migrations/20260801200000_fix_maps_scopes_default.exs @@ -18,9 +18,11 @@ defmodule WandererApp.Repo.Migrations.FixMapsScopesDefault do use Ecto.Migration - # Character codes of the literal string "{wormholes}", which is what the - # charlist default expanded to. - @bad_default ~w(123 119 111 114 109 104 111 108 101 115 125) + # Derived from the offending literal rather than transcribed by hand: a typo + # in a hardcoded list would make the WHERE clause match nothing and silently + # repair no rows. Both 20260331192521 and 20260406213852 used exactly this + # literal, so this single value catches rows written by either. + @bad_default Enum.map(~c"{wormholes}", &Integer.to_string/1) def up do alter table(:maps_v1) do @@ -32,6 +34,10 @@ defmodule WandererApp.Repo.Migrations.FixMapsScopesDefault do ) end + # Deliberately not a mirror image of up/0. Restoring the original charlist + # default would reintroduce the bug, so this drops the default instead, and + # rows already repaired stay repaired. Rolling back therefore leaves the + # schema in a different -- but correct -- state rather than the prior one. def down do alter table(:maps_v1) do modify :scopes, {:array, :text}, default: nil diff --git a/test/unit/map/map_scopes_test.exs b/test/unit/map/map_scopes_test.exs index aa929456..421342c2 100644 --- a/test/unit/map/map_scopes_test.exs +++ b/test/unit/map/map_scopes_test.exs @@ -78,7 +78,7 @@ defmodule WandererApp.Map.Server.MapScopesTest do # record missing solar_system_name and friends. Whether that surfaced # depended on suite order, which is why these failures moved with --seed. on_exit(fn -> - Enum.each(Elixir.Map.keys(test_systems), fn solar_system_id -> + Enum.each(Map.keys(test_systems), fn solar_system_id -> Cachex.del(:system_static_info_cache, solar_system_id) end) end) From 680e474589cc4c46bae75c8584a3c1828f3e9a95 Mon Sep 17 00:00:00 2001 From: Guarzo Date: Sat, 8 Aug 2026 11:04:33 -0400 Subject: [PATCH 07/10] ci: run only the integration-tagged tests in the integration job `mix test --include integration` adds the tagged tests to the full suite rather than selecting them, so the job re-ran all 926 tests -- roughly ten extra minutes of CI, and it re-reported the failures the main test job already covers. `--only` runs just the ~64 tagged tests. --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f8fcbf7..6dcbcab5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -397,4 +397,7 @@ jobs: mix ecto.migrate - name: Run integration tests - run: mix test --include integration + # --only, not --include: --include would run the whole suite on top of + # the tagged tests, duplicating the main test job's ~10 minutes and + # re-reporting failures it already covers. + run: mix test --only integration From becafec254d11a1fe9662d82011705914cbd84ed Mon Sep 17 00:00:00 2001 From: Guarzo Date: Sat, 8 Aug 2026 11:04:33 -0400 Subject: [PATCH 08/10] fix(ash): regenerate the maps_v1 snapshot for the scopes default The corrective migration set the `scopes` default to `["wormholes"]` and `map.ex` was updated to match, but the resource snapshot still recorded the old charlist default `'{wormholes}'`. Left that way, the next `mix ash_postgres.generate_migrations` would emit a second migration re-applying the same change on top of the corrective one. Regenerated with `mix ash.codegen`, keeping only the `maps_v1` snapshot and naming it for the existing corrective migration (20260801200000) rather than the codegen timestamp, since that migration already performs both the schema change and the data repair. Codegen also surfaces unrelated pre-existing drift in `map_chain_v1` (`locked_at` / `locked_by_id`, added on main by 20260425000000 without a snapshot regeneration). That is left alone here. --- .../repo/maps_v1/20260801200000.json | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 priv/resource_snapshots/repo/maps_v1/20260801200000.json diff --git a/priv/resource_snapshots/repo/maps_v1/20260801200000.json b/priv/resource_snapshots/repo/maps_v1/20260801200000.json new file mode 100644 index 00000000..1974941f --- /dev/null +++ b/priv/resource_snapshots/repo/maps_v1/20260801200000.json @@ -0,0 +1,277 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "slug", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "description", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "personal_note", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "public_api_key", + "type": "text" + }, + { + "allow_nil?": true, + "default": "[]", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "hubs", + "type": [ + "array", + "text" + ] + }, + { + "allow_nil?": false, + "default": "\"wormholes\"", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "scope", + "type": "text" + }, + { + "allow_nil?": true, + "default": "false", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "deleted", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "false", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "only_tracked_characters", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "options", + "type": "text" + }, + { + "allow_nil?": false, + "default": "false", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "webhooks_enabled", + "type": "boolean" + }, + { + "allow_nil?": false, + "default": "false", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "sse_enabled", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "[\"wormholes\"]", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "scopes", + "type": [ + "array", + "text" + ] + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "maps_v1_owner_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": null, + "table": "character_v1" + }, + "scale": null, + "size": null, + "source": "owner_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "F00091B5D3467DFD9D0B5DD8E5A5590E581D1F4D51F0D6CD07413358DC1AF2B7", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "maps_v1_unique_public_api_key_index", + "keys": [ + { + "type": "atom", + "value": "public_api_key" + } + ], + "name": "unique_public_api_key", + "nils_distinct?": true, + "where": null + }, + { + "all_tenants?": false, + "base_filter": null, + "index_name": "maps_v1_unique_slug_index", + "keys": [ + { + "type": "atom", + "value": "slug" + } + ], + "name": "unique_slug", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.WandererApp.Repo", + "schema": null, + "table": "maps_v1" +} \ No newline at end of file From c037929523105904e07894ec169144c1deb1d4e2 Mon Sep 17 00:00:00 2001 From: Guarzo Date: Sat, 8 Aug 2026 17:05:42 -0400 Subject: [PATCH 09/10] review: address pre-submit checklist findings Corrections from the upstream pre-submit review pass. --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6dcbcab5..e6c709a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -349,8 +349,10 @@ jobs: # history, so gating merges on them from day one is a policy change for # maintainers to make, not this PR. Remove it to turn them into a hard gate. integration-test: - name: Integration Tests (non-blocking) + name: Integration Tests runs-on: ubuntu-latest + # Temporary, pending #637: drop this line to make the job a hard gate. + # The check name stays stable so branch protection keeps matching it. continue-on-error: true services: From 963fe79b2bb252b1c72d489ec80d9f2ee4d8e507 Mon Sep 17 00:00:00 2001 From: Guarzo Date: Wed, 12 Aug 2026 14:01:40 -0400 Subject: [PATCH 10/10] ci: make the integration test job a hard gate #637 landed, so the integration suite is green and no longer needs the temporary continue-on-error escape hatch. --- .github/workflows/test.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 03debdf4..31d59a86 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -382,15 +382,12 @@ jobs: # They run here as a separate job rather than by changing the default # exclusion, so local `mix test` stays fast. # - # continue-on-error is deliberate and temporary: these ~64 tests have no CI - # history, so gating merges on them from day one is a policy change for - # maintainers to make, not this PR. Remove it to turn them into a hard gate. + # This is a hard gate: a red integration run blocks the merge. Note that 30 of + # the tests `--only integration` selects are `@tag :skip`, so the gate covers + # the 35 that actually execute. integration-test: name: Integration Tests runs-on: ubuntu-latest - # Temporary, pending #637: drop this line to make the job a hard gate. - # The check name stays stable so branch protection keeps matching it. - continue-on-error: true services: postgres: