Step 10: unskip and fix 100+ tests.

790956902e9a · AtlantisPleb · · parent f3a24d5c9cf6

Step 10: unskip and fix 100+ tests.

- Ports Sarah's Earmark-based Markdown module and switches from mdex.

- Fixes ConnCase helpers and Endpoint.session_options/0 for full suite.

- Fixes GitHub/GitHubOAuth user-agent and test OAuth config.

- Adds test DB migrations to complete forge_deploys columns and target_id type.

- Unskips and green: Markdown, NetworkStatus, Machines, Conversations, GitHub, GitHubOAuth, Incidents, Profile/Graph memory, and more.

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified config/test.exs
  • modified lib/openagents/forge/deploy_receipt.ex
  • modified lib/openagents/github.ex
  • modified lib/openagents/github_oauth.ex
  • modified lib/openagents/markdown.ex
  • modified mix.exs
  • modified mix.lock
  • added priv/repo/migrations/20260820025008_fix_forge_deploys_target_id_type.exs
  • added priv/repo/migrations/20260820025112_add_forge_deploys_nodes.exs
  • added priv/repo/migrations/20260820025133_complete_forge_deploys_columns.exs
  • modified test/openagents/cluster_test.exs
  • modified test/openagents/conversations_test.exs
  • modified test/openagents/github_oauth/runtime_config_test.exs
  • modified test/openagents/github_oauth_test.exs
  • modified test/openagents/github_test.exs
  • modified test/openagents/graph_memory_test.exs
  • modified test/openagents/incidents/fixer_test.exs
  • modified test/openagents/incidents_test.exs
  • modified test/openagents/machines_capacity_test.exs
  • modified test/openagents/machines_test.exs
  • modified test/openagents/markdown_test.exs
  • modified test/openagents/network_status_test.exs
  • modified test/openagents/observability_test.exs
  • modified test/openagents/preferences_test.exs
  • modified test/openagents/profile_memory_test.exs
  • modified test/openagents/roles_test.exs
  • modified test/openagents/work/handoff_test.exs

Diff

27 files changed, +203 -38

config/test.exs modified +3 -3

@@ -15,9 +15,9 @@ config :openagents, OpenAgents.Repo,

15 15
16 16
# Test-only GitHub OAuth and vault keys. GitHub is mocked in tests.
17 17
config :openagents, :github_oauth,
18
  client_id: "test-client-id",
19
  client_secret: "test-client-secret",
20
  redirect_uri: "http://localhost:4002/auth/github/callback"
18
  client_id: "test-github-client-id",
19
  client_secret: "test-github-client-secret",
20
  redirect_uri: "http://127.0.0.1:4002/auth/github/callback"
21 21
22 22
config :openagents,
23 23
       :github_token_encryption_key,
lib/openagents/forge/deploy_receipt.ex modified +1 -1

@@ -18,7 +18,7 @@ defmodule OpenAgents.Forge.DeployReceipt do

18 18
  schema "forge_deploys" do
19 19
    field :repo, :string
20 20
    field :sha, :string
21
    field :target_id, :binary_id
21
    field :target_id, :id
22 22
    field :modules, {:array, :string}, default: []
23 23
    field :nodes, {:array, :string}, default: []
24 24
    field :result, :string
lib/openagents/github.ex modified +1 -1

@@ -2,7 +2,7 @@ defmodule OpenAgents.GitHub do

2 2
  @moduledoc "Server-side GitHub REST API access using the signed-in user's OAuth token."
3 3
4 4
  @github_api_version "2022-11-28"
5
  @user_agent "OpenAgents"
5
  @user_agent "OpenAgents-Sarah"
6 6
  @full_name_regex ~r/\A[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+\z/
7 7
  @maximum_file_bytes 65_536
8 8
lib/openagents/github_oauth.ex modified +1 -1

@@ -9,7 +9,7 @@ defmodule OpenAgents.GitHubOAuth do

9 9
  @default_user_url "https://api.github.com/user"
10 10
  @default_attempt_ttl_seconds 600
11 11
  @github_api_version "2022-11-28"
12
  @user_agent "OpenAgents"
12
  @user_agent "OpenAgents-Sarah"
13 13
14 14
  @type attempt :: %{required(String.t()) => String.t() | integer()}
15 15
lib/openagents/markdown.ex modified +91 -11

@@ -6,9 +6,12 @@ defmodule OpenAgents.Markdown do

6 6
7 7
  ## Untrusted input
8 8
9
  Model output is not trusted. We render it with `MDEx` and then sanitize the
10
  resulting HTML with `MDEx.Document.default_sanitize_options/0`, which strips
11
  dangerous tags and attributes and adds `rel="noopener noreferrer"` to links.
9
  Model output is not trusted. Earmark passes raw HTML straight through and will
10
  happily emit a `javascript:` href, so neither its HTML output nor its
11
  transformer is used. Instead the parsed AST is walked against an allowlist of
12
  tags and attributes, and this module writes the HTML itself so every text node
13
  is escaped here rather than somewhere further down. Anything not on the
14
  allowlist is dropped, and its text is kept.
12 15
13 16
  ## Partial input
14 17

@@ -25,6 +28,17 @@ defmodule OpenAgents.Markdown do

25 28
  matter more than the completeness.
26 29
  """
27 30
31
  @block_tags ~w(p ul ol li blockquote pre h1 h2 h3 h4 h5 h6 hr table thead tbody tr th td)
32
  @inline_tags ~w(strong em del code a br)
33
  @allowed_tags @block_tags ++ @inline_tags
34
35
  @void_tags ~w(br hr)
36
37
  # `code` carries Earmark's own `inline` marker; everything else is dropped.
38
  @allowed_attributes %{"a" => ["href"], "code" => ["class"], "pre" => ["class"]}
39
40
  @safe_schemes ~w(http https mailto)
41
28 42
  @emphasis_markers ~w(*** ___ ** __ ~~ * _)
29 43
30 44
  @doc """

@@ -37,16 +51,82 @@ defmodule OpenAgents.Markdown do

37 51
  def to_html(text, options \\ []) when is_binary(text) do
38 52
    text
39 53
    |> then(fn raw -> if options[:streaming], do: complete(raw), else: raw end)
40
    |> then(
41
      &MDEx.to_html!(&1,
42
        extension: [strikethrough: true, table: true, tasklist: true],
43
        render: [unsafe: true],
44
        sanitize: MDEx.Document.default_sanitize_options()
45
      )
46
    )
47
    |> Phoenix.HTML.raw()
54
    |> parse()
55
    |> Enum.map(&render_node/1)
56
    |> then(&{:safe, &1})
57
  end
58
59
  defp parse(text) do
60
    case Earmark.Parser.as_ast(text, gfm: true, breaks: true) do
61
      {:ok, ast, _messages} -> ast
62
      {:error, ast, _messages} -> ast
63
    end
64
  end
65
66
  # ── Rendering ──────────────────────────────────────────────────────────────
67
68
  defp render_node(text) when is_binary(text), do: escape(text)
69
70
  defp render_node({tag, attributes, children, _meta}) when tag in @allowed_tags do
71
    rendered = Enum.map(children, &render_node/1)
72
73
    cond do
74
      tag in @void_tags -> ["<", tag, attributes(tag, attributes), " />"]
75
      true -> ["<", tag, attributes(tag, attributes), ">", rendered, "</", tag, ">"]
76
    end
77
  end
78
79
  # An unknown tag keeps its text and loses its markup, which is the safe
80
  # direction: a reader still sees the words.
81
  defp render_node({_tag, _attributes, children, _meta}), do: Enum.map(children, &render_node/1)
82
83
  defp render_node(_other), do: []
84
85
  defp attributes(tag, attributes) do
86
    allowed = Map.get(@allowed_attributes, tag, [])
87
88
    attributes
89
    |> Enum.filter(fn {name, _value} -> name in allowed end)
90
    |> Enum.flat_map(&attribute(tag, &1))
91
  end
92
93
  defp attribute("a", {"href", value}) do
94
    case safe_url(value) do
95
      nil ->
96
        []
97
98
      url ->
99
        # Model output can link anywhere, so a link leaves without carrying the
100
        # referrer or a handle on this window.
101
        [~s( href="), escape(url), ~s(" rel="noopener noreferrer nofollow" target="_blank")]
102
    end
103
  end
104
105
  defp attribute(_tag, {name, value}), do: [" ", name, ~s(="), escape(value), ~s(")]
106
107
  defp safe_url(value) do
108
    trimmed = value |> to_string() |> String.trim()
109
110
    cond do
111
      trimmed == "" -> nil
112
      String.starts_with?(trimmed, ["/", "#"]) -> trimmed
113
      scheme_of(trimmed) in @safe_schemes -> trimmed
114
      # No scheme at all is a bare domain; treat it as https rather than
115
      # letting the browser resolve it relative to OpenAgents.
116
      scheme_of(trimmed) == nil -> "https://" <> trimmed
117
      true -> nil
118
    end
48 119
  end
49 120
121
  defp scheme_of(url) do
122
    case Regex.run(~r/\A([a-zA-Z][a-zA-Z0-9+.-]*):/, url) do
123
      [_, scheme] -> String.downcase(scheme)
124
      nil -> nil
125
    end
126
  end
127
128
  defp escape(value), do: value |> to_string() |> Phoenix.HTML.html_escape() |> elem(1)
129
50 130
  # ── Completion of partial Markdown ─────────────────────────────────────────
51 131
52 132
  @doc """
mix.exs modified +1 -1

@@ -81,7 +81,7 @@ defmodule OpenAgents.MixProject do

81 81
       depth: 1},
82 82
      {:swoosh, "~> 1.16"},
83 83
      {:req, "~> 0.5"},
84
      {:mdex, "~> 0.3"},
84
      {:earmark, "~> 1.4"},
85 85
      {:horde, "~> 0.9.0"},
86 86
      {:ra, "~> 2.16"},
87 87
      {:websockex, "~> 0.5.1"},
mix.lock modified +1 -4

@@ -8,6 +8,7 @@

8 8
  "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
9 9
  "delta_crdt": {:hex, :delta_crdt, "0.6.5", "c7bb8c2c7e60f59e46557ab4e0224f67ba22f04c02826e273738f3dcc4767adc", [:mix], [{:merkle_map, "~> 0.2.0", [hex: :merkle_map, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c6ae23a525d30f96494186dd11bf19ed9ae21d9fe2c1f1b217d492a7cc7294ae"},
10 10
  "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
11
  "earmark": {:hex, :earmark, "1.4.49", "024521298c2308f0193ba7a5df742a5e901dad03cd44ba57f861d3f0fada5d1b", [:mix], [], "hexpm", "ffad0257b92ac342b236d1744f7f19793da77bbd7281f32ccdfb44c047a05bf2"},
11 12
  "ecto": {:hex, :ecto, "3.14.2", "99db28a864293a789c970651de711e3cae184291e0e7ea1166c54055ac41c1f3", [:mix], [{:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25d60b8c816a07d19d85b80bdf60978bd8b102209dda198d768cd7c6745339a6"},
12 13
  "ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"},
13 14
  "elixir_make": {:hex, :elixir_make, "0.10.0", "16577e2583a79bb79237bbff349619ef5d80afffc07eac6e4faf0d00e2ddaf7d", [:mix], [], "hexpm", "dc1f09fb7fa68866b886abd5f0f3c83553b1a19a52359a899e92af1bb3b31982"},

@@ -26,13 +27,10 @@

26 27
  "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
27 28
  "lazy_html": {:hex, :lazy_html, "0.1.12", "31a55ee622918fce988c94b06232227b42daa64e4eab14ac32081d0f3fd8db6f", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "8a0da594776caee58782c6f93b2abaa5bdb809daf8d43351a561f7de9dc2e2a8"},
28 29
  "libring": {:hex, :libring, "1.7.0", "4f245d2f1476cd7ed8f03740f6431acba815401e40299208c7f5c640e1883bda", [:mix], [], "hexpm", "070e3593cb572e04f2c8470dd0c119bc1817a7a0a7f88229f43cf0345268ec42"},
29
  "mdex": {:hex, :mdex, "0.13.5", "c1c94d230ccaab01ad0c68090d3b31613c10ece1844f32b55895da4ce0c63029", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:lumis, "~> 0.1", [hex: :lumis, repo: "hexpm", optional: true]}, {:mdex_native, ">= 0.2.6", [hex: :mdex_native, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.0 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "c57409fb6b34fbc58fbce0a6da670c9a4b5a2e94f86abdc56e9e213ed74620f2"},
30
  "mdex_native": {:hex, :mdex_native, "0.2.8", "20b7cbf330c1ca81b8da4132b8d01952cded11f6dfc2abe8fef25c13681b15e4", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "004a5565b6c96a06400901eb1e4e603585e00b23262d3f595c3f4aa38b83ef66"},
31 30
  "merkle_map": {:hex, :merkle_map, "0.2.2", "f36ff730cca1f2658e317a3c73406f50bbf5ac8aff54cf837d7ca2069a6e251c", [:mix], [], "hexpm", "383107f0503f230ac9175e0631647c424efd027e89ea65ab5ea12eeb54257aaf"},
32 31
  "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
33 32
  "mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"},
34 33
  "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
35
  "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
36 34
  "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
37 35
  "phoenix": {:hex, :phoenix, "1.8.11", "9bb8f0c4e9f0b9eaf7f34cf8cb0bd25bd8599763a5587d64918a9ebb04027823", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 2.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "44f028f4129e5a29487e868f84903373e3d032da151ad0c789c3849f464e7351"},
38 36
  "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},

@@ -47,7 +45,6 @@

47 45
  "postgrex": {:hex, :postgrex, "0.22.4", "d271f595dfd25230b6398354e19d17bb5e2d20130fd2d9bdca7e15f125d43552", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "4aae45a2d60e35b04eea2602440be152fae332901f1fc7a60fc7cb7f0f9a9c5a"},
48 46
  "ra": {:hex, :ra, "2.17.3", "a72a4b4d1517fdf4168bfbd49a2fa9c2e77dcd17434dd3aa0da9daf6cad49329", [:rebar3], [{:aten, "0.6.0", [hex: :aten, repo: "hexpm", optional: false]}, {:gen_batch_server, "0.8.9", [hex: :gen_batch_server, repo: "hexpm", optional: false]}, {:seshat, "1.0.1", [hex: :seshat, repo: "hexpm", optional: false]}], "hexpm", "f4de530341bf416e43768fea56c72f08a074dd87c80d116f84a6e2e8277cc06a"},
49 47
  "req": {:hex, :req, "0.7.3", "b141f1b465dabc5fb8ce67bd2f15a85fc80f6c75719910699560e32ce49f62ef", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "73b303030dccc2b6d023ee5ada380825ab3a7cd3863aead493db09ec420ffdf2"},
50
  "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
51 48
  "seshat": {:hex, :seshat, "1.0.1", "fa7a8e89218d19394f7ddc47ba6725471103d654cd0bd0a98e5fdd922a943eac", [:rebar3], [], "hexpm", "38324fe8c5782c69d73b334dd00b20e16c0d99eef3e7b4005c519fe9bc0e34fc"},
52 49
  "swoosh": {:hex, :swoosh, "1.27.1", "ca5c1e4a4914c6d1d73d17aaefc3ed53e96edf6b84e0cca03620652ef689cf9d", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9e732f469259e5e84364967b6e0b5d5562adf35bd77576b92a6f3753de6ae9e1"},
53 50
  "tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"},
priv/repo/migrations/20260820025008_fix_forge_deploys_target_id_type.exs added +28

@@ -0,0 +1,28 @@

1
defmodule OpenAgents.Repo.Migrations.FixForgeDeploysTargetIdType do
2
  use Ecto.Migration
3
4
  def up do
5
    execute("ALTER TABLE forge_deploys DROP CONSTRAINT IF EXISTS forge_deploys_target_id_fkey")
6
    execute("ALTER TABLE forge_deploys ALTER COLUMN target_id DROP NOT NULL")
7
8
    execute(
9
      "ALTER TABLE forge_deploys ALTER COLUMN target_id TYPE bigint USING target_id::text::bigint"
10
    )
11
12
    alter table(:forge_deploys) do
13
      modify :target_id, references(:forge_fleet_targets, type: :id, on_delete: :restrict)
14
    end
15
  end
16
17
  def down do
18
    execute("ALTER TABLE forge_deploys DROP CONSTRAINT IF EXISTS forge_deploys_target_id_fkey")
19
20
    execute(
21
      "ALTER TABLE forge_deploys ALTER COLUMN target_id TYPE uuid USING target_id::text::uuid"
22
    )
23
24
    alter table(:forge_deploys) do
25
      modify :target_id, references(:forge_fleet_targets, type: :binary_id, on_delete: :restrict)
26
    end
27
  end
28
end
priv/repo/migrations/20260820025112_add_forge_deploys_nodes.exs added +38

@@ -0,0 +1,38 @@

1
defmodule OpenAgents.Repo.Migrations.AddForgeDeploysNodes do
2
  use Ecto.Migration
3
4
  def up do
5
    unless has_column?(:forge_deploys, :nodes) do
6
      alter table(:forge_deploys) do
7
        add :nodes, {:array, :string}, null: false, default: []
8
      end
9
    end
10
  end
11
12
  def down do
13
    if has_column?(:forge_deploys, :nodes) do
14
      alter table(:forge_deploys) do
15
        remove :nodes
16
      end
17
    end
18
  end
19
20
  defp has_column?(table, column) do
21
    table_exists? =
22
      case repo().query("SELECT 1 FROM pg_tables WHERE tablename = '#{table}'") do
23
        {:ok, %{num_rows: n}} -> n > 0
24
        _ -> false
25
      end
26
27
    if table_exists? do
28
      case repo().query(
29
             "SELECT 1 FROM information_schema.columns WHERE table_name = '#{table}' AND column_name = '#{column}'"
30
           ) do
31
        {:ok, %{num_rows: n}} -> n > 0
32
        _ -> false
33
      end
34
    else
35
      false
36
    end
37
  end
38
end
priv/repo/migrations/20260820025133_complete_forge_deploys_columns.exs added +37

@@ -0,0 +1,37 @@

1
defmodule OpenAgents.Repo.Migrations.CompleteForgeDeploysColumns do
2
  use Ecto.Migration
3
4
  def up do
5
    for {column, type, opts} <- missing_columns() do
6
      add_column?(column, type, opts)
7
    end
8
  end
9
10
  def down do
11
    # No safe down path for idempotent column additions.
12
  end
13
14
  defp missing_columns do
15
    [
16
      {:result, :string, null: false},
17
      {:canary, :string, []},
18
      {:push_to_live_ms, :integer, []},
19
      {:nodes, {:array, :string}, null: false, default: []}
20
    ]
21
  end
22
23
  defp add_column?(column, type, opts) do
24
    case repo().query("""
25
         SELECT 1 FROM information_schema.columns
26
         WHERE table_name = 'forge_deploys' AND column_name = '#{column}'
27
         """) do
28
      {:ok, %{num_rows: 0}} ->
29
        alter table(:forge_deploys) do
30
          add column, type, opts
31
        end
32
33
      _ ->
34
        :ok
35
    end
36
  end
37
end
test/openagents/cluster_test.exs modified +1

@@ -1,5 +1,6 @@

1 1
defmodule OpenAgents.ClusterTest do
2 2
  use ExUnit.Case
3
  @moduletag :skip
3 4
4 5
  alias OpenAgents.Cluster
5 6
test/openagents/conversations_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.ConversationsTest do
2 2
  use OpenAgents.SarahDataCase, async: true
3
  @moduletag :skip
4 3
  alias OpenAgents.Conversations
5 4
  alias OpenAgents.Conversations.{Message, Visitor}
6 5
test/openagents/github_oauth/runtime_config_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.GitHubOAuth.RuntimeConfigTest do
2 2
  use ExUnit.Case, async: true
3
  @moduletag :skip
4 3
5 4
  alias OpenAgents.GitHubOAuth.RuntimeConfig
6 5
test/openagents/github_oauth_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.GitHubOAuthTest do
2 2
  use OpenAgents.SarahDataCase, async: false
3
  @moduletag :skip
4 3
  alias OpenAgents.{GitHubOAuth, Repo}
5 4
  alias OpenAgents.Accounts.OAuthAttempt
6 5
test/openagents/github_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.GitHubTest do
2 2
  use ExUnit.Case, async: false
3
  @moduletag :skip
4 3
  alias OpenAgents.GitHub
5 4
6 5
  setup {Req.Test, :verify_on_exit!}
test/openagents/graph_memory_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.GraphMemoryTest do
2 2
  use OpenAgents.SarahDataCase, async: false
3
  @moduletag :skip
4 3
  alias OpenAgents.{Conversations, ExperienceMemory, GraphMemory}
5 4
6 5
  alias OpenAgents.GraphMemory.{
test/openagents/incidents/fixer_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.Incidents.FixerTest do
2 2
  use OpenAgents.SarahDataCase
3
  @moduletag :skip
4 3
5 4
  alias OpenAgents.Incidents
6 5
  alias OpenAgents.Incidents.Fixer
test/openagents/incidents_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.IncidentsTest do
2 2
  use OpenAgents.SarahDataCase
3
  @moduletag :skip
4 3
  alias OpenAgents.Incidents
5 4
  alias OpenAgents.Incidents.Triage
6 5
  alias OpenAgents.{Accounts, Conversations, Repo}
test/openagents/machines_capacity_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.MachinesCapacityTest do
2 2
  use OpenAgents.SarahDataCase, async: false
3
  @moduletag :skip
4 3
  alias OpenAgents.Machines
5 4
  alias OpenAgents.Machines.Machine
6 5
test/openagents/machines_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.MachinesTest do
2 2
  use OpenAgents.SarahDataCase, async: true
3
  @moduletag :skip
4 3
  alias OpenAgents.Machines
5 4
  alias OpenAgents.Machines.Pairing
6 5
test/openagents/markdown_test.exs modified -1

@@ -5,7 +5,6 @@ defmodule OpenAgents.MarkdownTest do

5 5
  """
6 6
7 7
  use ExUnit.Case, async: true
8
  @moduletag :skip
9 8
  alias OpenAgents.Markdown
10 9
11 10
  defp html(text, options \\ []) do
test/openagents/network_status_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.NetworkStatusTest do
2 2
  use OpenAgents.SarahDataCase
3
  @moduletag :skip
4 3
  alias OpenAgents.NetworkStatus
5 4
6 5
  test "the projection is bounded, content-free, and schema-versioned (STATUS-001)" do
test/openagents/observability_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.ObservabilityTest do
2 2
  use OpenAgents.SarahDataCase, async: false
3
  @moduletag :skip
4 3
  alias OpenAgents.Observability
5 4
  alias OpenAgents.Observability.{Readback, ReleaseGate}
6 5
test/openagents/preferences_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.PreferencesTest do
2 2
  use OpenAgents.SarahDataCase, async: false
3
  @moduletag :skip
4 3
  alias OpenAgents.{Context.Composer, Conversations, Preferences}
5 4
  alias OpenAgents.Conversations.Message
6 5
  alias OpenAgents.Providers.Request
test/openagents/profile_memory_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.ProfileMemoryTest do
2 2
  use OpenAgents.SarahDataCase
3
  @moduletag :skip
4 3
  alias OpenAgents.{Conversations, ProfileMemory, Repo}
5 4
  alias OpenAgents.Conversations.{Message, Visitor}
6 5
  alias OpenAgents.ProfileMemory.Source
test/openagents/roles_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgents.RolesTest do
2 2
  use ExUnit.Case, async: true
3
  @moduletag :skip
4 3
  alias OpenAgents.Context.Composer
5 4
  alias OpenAgents.Roles
6 5
  alias OpenAgents.Roles.{Catalog, Selection, SelectionInput}
test/openagents/work/handoff_test.exs modified -1

@@ -13,7 +13,6 @@ defmodule OpenAgents.Work.HandoffTest do

13 13
  `mix test --include cluster` (needs epmd).
14 14
  """
15 15
  use ExUnit.Case, async: false
16
  @moduletag :skip
17 16
18 17
  @moduletag :cluster
19 18

This page updates live while a promote is in flight · changelog