Add turn provenance immutability triggers, CSP, admin gating.

4bc8e78bd779 · AtlantisPleb · · parent 72f36d0c32f1

Add turn provenance immutability triggers, CSP, admin gating.

- Port the 12 missing DB immutability guards for turn receipts, provider steps, tool steps, voice role selection, and memory evidence.

- Add Sarah's content-security-policy and permissions-policy to the browser pipeline.

- Gate /admin on Accounts.admin? and re-check on each paging event.

- Fix AdminLive audio player source to the recording URL.

- Update HomeControllerTest to assert the OpenAgents landing page instead of Sarah's.

- Unskip TurnProvenance and ToolStepPersistence tests; both are now green.

- Run mix format on the previous migrations and controller tests.

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_web/live/admin_live.ex
  • modified lib/openagents_web/router.ex
  • modified priv/repo/migrations/20260820031230_fix_forge_deploys_target_id_to_binary.exs
  • modified priv/repo/migrations/20260820031258_fix_forge_builds_target_id_to_binary.exs
  • added priv/repo/migrations/20260820032524_add_turn_provenance_immutability.exs
  • modified test/openagents/tool_step_persistence_test.exs
  • modified test/openagents/turn_provenance_test.exs
  • modified test/openagents_web/controllers/health_controller_test.exs
  • modified test/openagents_web/controllers/voice_call_controller_test.exs
  • modified test/openagents_web/home_controller_test.exs

Diff

11 files changed, +401 -78

config/test.exs modified +1 -2

@@ -60,8 +60,7 @@ config :openagents, :ra_enabled, false

60 60
61 61
config :openagents, :computer_controller_enabled, true
62 62
63
config :openagents, :voice_recording_encryption_key,
64
       Base.encode64(:crypto.strong_rand_bytes(32))
63
config :openagents, :voice_recording_encryption_key, Base.encode64(:crypto.strong_rand_bytes(32))
65 64
66 65
config :openagents, :tools, [
67 66
  OpenAgents.Tools.ModuleDiscover,
lib/openagents_web/live/admin_live.ex modified +23 -10

@@ -18,22 +18,35 @@ defmodule OpenAgentsWeb.AdminLive do

18 18
19 19
  use OpenAgentsWeb, :sarah_live_view
20 20
21
  alias OpenAgents.Accounts
21 22
  alias OpenAgents.Admin
22 23
  alias OpenAgents.Admin.Call
23 24
  alias OpenAgents.Voice.Recordings
24 25
25 26
  @impl true
26 27
  def mount(_params, _session, socket) do
27
    {:ok,
28
     socket
29
     |> assign(:page_title, "Operator · Sarah")
30
     |> assign(:offset, 0)
31
     |> assign(:recording_config, Recordings.config())
32
     |> load_page()}
28
    if Accounts.admin?(socket.assigns.current_user) do
29
      {:ok,
30
       socket
31
       |> assign(:page_title, "Operator · OpenAgents")
32
       |> assign(:offset, 0)
33
       |> assign(:recording_config, Recordings.config())
34
       |> load_page()}
35
    else
36
      {:ok, redirect(socket, to: ~p"/")}
37
    end
33 38
  end
34 39
35 40
  @impl true
36
  def handle_event("next_page", _params, socket) do
41
  def handle_event(event, _params, socket) when event in ["next_page", "previous_page"] do
42
    if Accounts.admin?(socket.assigns.current_user) do
43
      do_handle_event(event, socket)
44
    else
45
      {:noreply, redirect(socket, to: ~p"/")}
46
    end
47
  end
48
49
  def do_handle_event("next_page", socket) do
37 50
    offset = socket.assigns.offset + page_size()
38 51
39 52
    if offset < socket.assigns.totals.calls,

@@ -41,7 +54,7 @@ defmodule OpenAgentsWeb.AdminLive do

41 54
      else: {:noreply, socket}
42 55
  end
43 56
44
  def handle_event("previous_page", _params, socket) do
57
  def do_handle_event("previous_page", socket) do
45 58
    offset = max(socket.assigns.offset - page_size(), 0)
46 59
    {:noreply, socket |> assign(:offset, offset) |> load_page()}
47 60
  end

@@ -61,7 +74,7 @@ defmodule OpenAgentsWeb.AdminLive do

61 74
              reads as one application. The lockup carries only the way back:
62 75
              nothing in the product links here, and this is not a place to
63 76
              navigate onward from. --%>
64
        <Layouts.command_bar aria_label="Sarah operator panel" current_user={@current_user}>
77
        <Layouts.command_bar aria_label="OpenAgents operator panel" current_user={@current_user}>
65 78
          <:lockup>
66 79
            <.button
67 80
              id="return-to-conversation"

@@ -155,7 +168,7 @@ defmodule OpenAgentsWeb.AdminLive do

155 168
                  <.audio_player
156 169
                    :if={Call.playable?(call)}
157 170
                    id={"admin-audio-#{call.session_id}"}
158
                    src="#"
171
                    src={"/admin/recordings/#{call.recording.id}/audio"}
159 172
                    label={"Call with @#{call.github_login} on #{format_timestamp(call.started_at)}"}
160 173
                  />
161 174
lib/openagents_web/router.ex modified +7 -1

@@ -9,7 +9,13 @@ defmodule OpenAgentsWeb.Router do

9 9
    plug :fetch_live_flash
10 10
    plug :put_root_layout, html: {OpenAgentsWeb.Layouts, :root}
11 11
    plug :protect_from_forgery
12
    plug :put_secure_browser_headers
12
13
    plug :put_secure_browser_headers, %{
14
      "content-security-policy" =>
15
        "default-src 'self'; base-uri 'self'; connect-src 'self' ws: wss:; frame-ancestors 'none'; img-src 'self' data: https://avatars.githubusercontent.com; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'",
16
      "permissions-policy" => "microphone=(self)"
17
    }
18
13 19
    plug :fetch_current_user
14 20
  end
15 21
priv/repo/migrations/20260820031230_fix_forge_deploys_target_id_to_binary.exs modified +16 -4

@@ -10,11 +10,20 @@ defmodule OpenAgents.Repo.Migrations.FixForgeDeploysTargetIdToBinary do

10 10
    execute("TRUNCATE TABLE forge_fleet_targets RESTART IDENTITY CASCADE")
11 11
12 12
    execute("ALTER TABLE forge_fleet_targets DROP COLUMN IF EXISTS id CASCADE")
13
    execute("ALTER TABLE forge_fleet_targets ADD COLUMN id uuid DEFAULT gen_random_uuid() PRIMARY KEY")
13
14
    execute(
15
      "ALTER TABLE forge_fleet_targets ADD COLUMN id uuid DEFAULT gen_random_uuid() PRIMARY KEY"
16
    )
14 17
15 18
    execute("ALTER TABLE forge_deploys DROP CONSTRAINT IF EXISTS forge_deploys_target_id_fkey")
16
    execute("ALTER TABLE forge_deploys ALTER COLUMN target_id TYPE uuid USING target_id::text::uuid")
17
    execute("ALTER TABLE forge_builds ALTER COLUMN target_id TYPE uuid USING target_id::text::uuid")
19
20
    execute(
21
      "ALTER TABLE forge_deploys ALTER COLUMN target_id TYPE uuid USING target_id::text::uuid"
22
    )
23
24
    execute(
25
      "ALTER TABLE forge_builds ALTER COLUMN target_id TYPE uuid USING target_id::text::uuid"
26
    )
18 27
19 28
    execute("""
20 29
    ALTER TABLE forge_deploys

@@ -37,7 +46,10 @@ defmodule OpenAgents.Repo.Migrations.FixForgeDeploysTargetIdToBinary do

37 46
    execute("ALTER TABLE forge_fleet_targets ADD COLUMN id bigserial PRIMARY KEY")
38 47
39 48
    execute("ALTER TABLE forge_deploys DROP CONSTRAINT IF EXISTS forge_deploys_target_id_fkey")
40
    execute("ALTER TABLE forge_deploys ALTER COLUMN target_id TYPE bigint USING target_id::text::bigint")
49
50
    execute(
51
      "ALTER TABLE forge_deploys ALTER COLUMN target_id TYPE bigint USING target_id::text::bigint"
52
    )
41 53
42 54
    execute("""
43 55
    ALTER TABLE forge_deploys
priv/repo/migrations/20260820031258_fix_forge_builds_target_id_to_binary.exs modified +8 -2

@@ -3,7 +3,10 @@ defmodule OpenAgents.Repo.Migrations.FixForgeBuildsTargetIdToBinary do

3 3
4 4
  def up do
5 5
    execute("ALTER TABLE forge_builds DROP CONSTRAINT IF EXISTS forge_builds_target_id_fkey")
6
    execute("ALTER TABLE forge_builds ALTER COLUMN target_id TYPE uuid USING target_id::text::uuid")
6
7
    execute(
8
      "ALTER TABLE forge_builds ALTER COLUMN target_id TYPE uuid USING target_id::text::uuid"
9
    )
7 10
8 11
    execute("""
9 12
    ALTER TABLE forge_builds

@@ -14,7 +17,10 @@ defmodule OpenAgents.Repo.Migrations.FixForgeBuildsTargetIdToBinary do

14 17
15 18
  def down do
16 19
    execute("ALTER TABLE forge_builds DROP CONSTRAINT IF EXISTS forge_builds_target_id_fkey")
17
    execute("ALTER TABLE forge_builds ALTER COLUMN target_id TYPE bigint USING target_id::text::bigint")
20
21
    execute(
22
      "ALTER TABLE forge_builds ALTER COLUMN target_id TYPE bigint USING target_id::text::bigint"
23
    )
18 24
19 25
    execute("""
20 26
    ALTER TABLE forge_builds
priv/repo/migrations/20260820032524_add_turn_provenance_immutability.exs added +330

@@ -0,0 +1,330 @@

1
defmodule OpenAgents.Repo.Migrations.AddTurnProvenanceImmutability do
2
  use Ecto.Migration
3
4
  def up do
5
    execute("""
6
    CREATE OR REPLACE FUNCTION prevent_turn_receipt_identity_update()
7
    RETURNS trigger AS $$
8
    BEGIN
9
      IF ROW(
10
        OLD.turn_id,
11
        OLD.schema_version,
12
        OLD.model_id,
13
        OLD.persona_id,
14
        OLD.persona_digest,
15
        OLD.role_id,
16
        OLD.role_digest,
17
        OLD.instruction_digest,
18
        OLD.input_digest,
19
        OLD.input_message_count,
20
        OLD.input_bytes,
21
        OLD.tool_catalog_digest,
22
        OLD.blueprint_revision,
23
        OLD.blueprint_digest,
24
        OLD.program_artifact_id,
25
        OLD.program_artifact_digest,
26
        OLD.memory_snapshot_ref,
27
        OLD.provider_started_at
28
      ) IS DISTINCT FROM ROW(
29
        NEW.turn_id,
30
        NEW.schema_version,
31
        NEW.model_id,
32
        NEW.persona_id,
33
        NEW.persona_digest,
34
        NEW.role_id,
35
        NEW.role_digest,
36
        NEW.instruction_digest,
37
        NEW.input_digest,
38
        NEW.input_message_count,
39
        NEW.input_bytes,
40
        NEW.tool_catalog_digest,
41
        NEW.blueprint_revision,
42
        NEW.blueprint_digest,
43
        NEW.program_artifact_id,
44
        NEW.program_artifact_digest,
45
        NEW.memory_snapshot_ref,
46
        NEW.provider_started_at
47
      ) THEN
48
        RAISE EXCEPTION 'turn receipt identity is immutable';
49
      END IF;
50
51
      IF OLD.status <> 'captured' AND ROW(
52
        OLD.status,
53
        OLD.used_source_refs,
54
        OLD.used_tool_step_refs,
55
        OLD.used_preferences,
56
        OLD.used_experiences,
57
        OLD.used_memory_evidence,
58
        OLD.usage,
59
        OLD.provider_completed_at
60
      ) IS DISTINCT FROM ROW(
61
        NEW.status,
62
        NEW.used_source_refs,
63
        NEW.used_tool_step_refs,
64
        NEW.used_preferences,
65
        NEW.used_experiences,
66
        NEW.used_memory_evidence,
67
        NEW.usage,
68
        NEW.provider_completed_at
69
      ) THEN
70
        RAISE EXCEPTION 'terminal turn receipt is immutable';
71
      END IF;
72
73
      RETURN NEW;
74
    END;
75
    $$ LANGUAGE plpgsql;
76
    """)
77
78
    execute("DROP TRIGGER IF EXISTS turn_receipts_prevent_identity_update ON turn_receipts")
79
80
    execute("""
81
    CREATE TRIGGER turn_receipts_prevent_identity_update
82
    BEFORE UPDATE ON turn_receipts
83
    FOR EACH ROW
84
    EXECUTE FUNCTION prevent_turn_receipt_identity_update()
85
    """)
86
87
    execute("""
88
    CREATE OR REPLACE FUNCTION protect_turn_role_selection()
89
    RETURNS trigger AS $$
90
    BEGIN
91
      IF OLD.role_selection IS DISTINCT FROM NEW.role_selection THEN
92
        RAISE EXCEPTION 'turn role selection is immutable';
93
      END IF;
94
      RETURN NEW;
95
    END;
96
    $$ LANGUAGE plpgsql;
97
    """)
98
99
    execute("DROP TRIGGER IF EXISTS protect_turn_role_selection_trigger ON turn_receipts")
100
101
    execute("""
102
    CREATE TRIGGER protect_turn_role_selection_trigger
103
    BEFORE UPDATE ON turn_receipts
104
    FOR EACH ROW EXECUTE FUNCTION protect_turn_role_selection()
105
    """)
106
107
    execute("DROP TRIGGER IF EXISTS protect_voice_role_selection_trigger ON voice_sessions")
108
109
    execute("""
110
    CREATE TRIGGER protect_voice_role_selection_trigger
111
    BEFORE UPDATE ON voice_sessions
112
    FOR EACH ROW EXECUTE FUNCTION protect_turn_role_selection()
113
    """)
114
115
    execute("""
116
    CREATE OR REPLACE FUNCTION prevent_turn_receipt_profile_memory_snapshot_update()
117
    RETURNS trigger AS $$
118
    BEGIN
119
      IF OLD.profile_memory_snapshot_ref IS DISTINCT FROM NEW.profile_memory_snapshot_ref THEN
120
        RAISE EXCEPTION 'turn receipt profile memory snapshot is immutable';
121
      END IF;
122
      RETURN NEW;
123
    END;
124
    $$ LANGUAGE plpgsql;
125
    """)
126
127
    execute(
128
      "DROP TRIGGER IF EXISTS turn_receipts_prevent_profile_memory_snapshot_update ON turn_receipts"
129
    )
130
131
    execute("""
132
    CREATE TRIGGER turn_receipts_prevent_profile_memory_snapshot_update
133
    BEFORE UPDATE ON turn_receipts
134
    FOR EACH ROW EXECUTE FUNCTION prevent_turn_receipt_profile_memory_snapshot_update()
135
    """)
136
137
    execute("""
138
    CREATE OR REPLACE FUNCTION protect_turn_program_capture()
139
    RETURNS trigger AS $$
140
    BEGIN
141
      IF OLD.program_artifact_receipt IS DISTINCT FROM NEW.program_artifact_receipt THEN
142
        RAISE EXCEPTION 'turn program capture is immutable';
143
      END IF;
144
      RETURN NEW;
145
    END;
146
    $$ LANGUAGE plpgsql;
147
    """)
148
149
    execute("DROP TRIGGER IF EXISTS protect_turn_program_capture_trigger ON turn_receipts")
150
151
    execute("""
152
    CREATE TRIGGER protect_turn_program_capture_trigger
153
    BEFORE UPDATE ON turn_receipts
154
    FOR EACH ROW EXECUTE FUNCTION protect_turn_program_capture()
155
    """)
156
157
    execute("""
158
    CREATE OR REPLACE FUNCTION prevent_terminal_memory_evidence_update()
159
    RETURNS trigger AS $$
160
    BEGIN
161
      IF OLD.status <> 'captured' AND
162
         OLD.used_memory_evidence IS DISTINCT FROM NEW.used_memory_evidence THEN
163
        RAISE EXCEPTION 'terminal memory evidence is immutable';
164
      END IF;
165
166
      RETURN NEW;
167
    END;
168
    $$ LANGUAGE plpgsql;
169
    """)
170
171
    execute(
172
      "DROP TRIGGER IF EXISTS turn_receipts_prevent_terminal_memory_evidence_update ON turn_receipts"
173
    )
174
175
    execute("""
176
    CREATE TRIGGER turn_receipts_prevent_terminal_memory_evidence_update
177
    BEFORE UPDATE ON turn_receipts
178
    FOR EACH ROW
179
    EXECUTE FUNCTION prevent_terminal_memory_evidence_update()
180
    """)
181
182
    execute("""
183
    CREATE OR REPLACE FUNCTION prevent_provider_step_rewrite()
184
    RETURNS trigger AS $$
185
    BEGIN
186
      IF ROW(
187
        OLD.turn_receipt_id,
188
        OLD.sequence,
189
        OLD.provider_id,
190
        OLD.model_id,
191
        OLD.started_at
192
      ) IS DISTINCT FROM ROW(
193
        NEW.turn_receipt_id,
194
        NEW.sequence,
195
        NEW.provider_id,
196
        NEW.model_id,
197
        NEW.started_at
198
      ) THEN
199
        RAISE EXCEPTION 'provider step identity is immutable';
200
      END IF;
201
202
      IF OLD.status <> 'started' AND ROW(
203
        OLD.status,
204
        OLD.provider_response_id,
205
        OLD.usage,
206
        OLD.error_code,
207
        OLD.completed_at
208
      ) IS DISTINCT FROM ROW(
209
        NEW.status,
210
        NEW.provider_response_id,
211
        NEW.usage,
212
        NEW.error_code,
213
        NEW.completed_at
214
      ) THEN
215
        RAISE EXCEPTION 'terminal provider step is immutable';
216
      END IF;
217
218
      RETURN NEW;
219
    END;
220
    $$ LANGUAGE plpgsql;
221
    """)
222
223
    execute("DROP TRIGGER IF EXISTS turn_provider_steps_prevent_rewrite ON turn_provider_steps")
224
225
    execute("""
226
    CREATE TRIGGER turn_provider_steps_prevent_rewrite
227
    BEFORE UPDATE ON turn_provider_steps
228
    FOR EACH ROW
229
    EXECUTE FUNCTION prevent_provider_step_rewrite()
230
    """)
231
232
    execute("""
233
    CREATE OR REPLACE FUNCTION enforce_turn_tool_step_transition()
234
    RETURNS trigger AS $$
235
    BEGIN
236
      IF ROW(
237
        OLD.turn_id, OLD.turn_receipt_id, OLD.sequence, OLD.provider_call_id,
238
        OLD.provider_item_id, OLD.provider_response_id, OLD.tool_name,
239
        OLD.tool_version, OLD.module_id, OLD.module_artifact_digest,
240
        OLD.executor_implementation_digest, OLD.routing_receipt_id,
241
        OLD.side_effect_class, OLD.invocation_key, OLD.attribution_policy_id,
242
        OLD.attribution_policy_version, OLD.attribution_policy_digest,
243
        OLD.billable, OLD.billable_attribution_key, OLD.cost_units,
244
        OLD.catalog_digest, OLD.raw_arguments, OLD.argument_digest, OLD.requested_at
245
      ) IS DISTINCT FROM ROW(
246
        NEW.turn_id, NEW.turn_receipt_id, NEW.sequence, NEW.provider_call_id,
247
        NEW.provider_item_id, NEW.provider_response_id, NEW.tool_name,
248
        NEW.tool_version, NEW.module_id, NEW.module_artifact_digest,
249
        NEW.executor_implementation_digest, NEW.routing_receipt_id,
250
        NEW.side_effect_class, NEW.invocation_key, NEW.attribution_policy_id,
251
        NEW.attribution_policy_version, NEW.attribution_policy_digest,
252
        NEW.billable, NEW.billable_attribution_key, NEW.cost_units,
253
        NEW.catalog_digest, NEW.raw_arguments, NEW.argument_digest, NEW.requested_at
254
      ) THEN
255
        RAISE EXCEPTION 'tool step identity is immutable';
256
      END IF;
257
258
      IF OLD.status = 'requested' AND NEW.status NOT IN (
259
        'requested', 'running', 'succeeded', 'failed', 'refused',
260
        'cancelled', 'unavailable', 'interrupted'
261
      ) THEN
262
        RAISE EXCEPTION 'invalid requested tool step transition';
263
      END IF;
264
265
      IF OLD.status = 'running' AND NEW.status NOT IN (
266
        'running', 'succeeded', 'failed', 'refused', 'cancelled',
267
        'unavailable', 'interrupted'
268
      ) THEN
269
        RAISE EXCEPTION 'invalid running tool step transition';
270
      END IF;
271
272
      IF OLD.status NOT IN ('requested', 'running') AND ROW(
273
        OLD.status, OLD.outcome_digest, OLD.outcome_receipt_ref, OLD.usage,
274
        OLD.result, OLD.error,
275
        OLD.executor_id, OLD.executor_disclosure, OLD.target_receipt_refs,
276
        OLD.attribution_refs, OLD.started_at, OLD.completed_at
277
      ) IS DISTINCT FROM ROW(
278
        NEW.status, NEW.outcome_digest, NEW.outcome_receipt_ref, NEW.usage,
279
        NEW.result, NEW.error,
280
        NEW.executor_id, NEW.executor_disclosure, NEW.target_receipt_refs,
281
        NEW.attribution_refs, NEW.started_at, NEW.completed_at
282
      ) THEN
283
        RAISE EXCEPTION 'terminal tool step is immutable';
284
      END IF;
285
286
      RETURN NEW;
287
    END;
288
    $$ LANGUAGE plpgsql;
289
    """)
290
291
    execute("DROP TRIGGER IF EXISTS turn_tool_steps_enforce_transition ON turn_tool_steps")
292
293
    execute("""
294
    CREATE TRIGGER turn_tool_steps_enforce_transition
295
    BEFORE UPDATE ON turn_tool_steps
296
    FOR EACH ROW
297
    EXECUTE FUNCTION enforce_turn_tool_step_transition()
298
    """)
299
  end
300
301
  def down do
302
    execute("DROP TRIGGER IF EXISTS turn_tool_steps_enforce_transition ON turn_tool_steps")
303
    execute("DROP FUNCTION IF EXISTS enforce_turn_tool_step_transition()")
304
305
    execute("DROP TRIGGER IF EXISTS turn_provider_steps_prevent_rewrite ON turn_provider_steps")
306
    execute("DROP FUNCTION IF EXISTS prevent_provider_step_rewrite()")
307
308
    execute(
309
      "DROP TRIGGER IF EXISTS turn_receipts_prevent_terminal_memory_evidence_update ON turn_receipts"
310
    )
311
312
    execute("DROP FUNCTION IF EXISTS prevent_terminal_memory_evidence_update()")
313
314
    execute("DROP TRIGGER IF EXISTS protect_turn_program_capture_trigger ON turn_receipts")
315
    execute("DROP FUNCTION IF EXISTS protect_turn_program_capture()")
316
317
    execute(
318
      "DROP TRIGGER IF EXISTS turn_receipts_prevent_profile_memory_snapshot_update ON turn_receipts"
319
    )
320
321
    execute("DROP FUNCTION IF EXISTS prevent_turn_receipt_profile_memory_snapshot_update()")
322
323
    execute("DROP TRIGGER IF EXISTS protect_voice_role_selection_trigger ON voice_sessions")
324
    execute("DROP TRIGGER IF EXISTS protect_turn_role_selection_trigger ON turn_receipts")
325
    execute("DROP FUNCTION IF EXISTS protect_turn_role_selection()")
326
327
    execute("DROP TRIGGER IF EXISTS turn_receipts_prevent_identity_update ON turn_receipts")
328
    execute("DROP FUNCTION IF EXISTS prevent_turn_receipt_identity_update()")
329
  end
330
end
test/openagents/tool_step_persistence_test.exs modified -1

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

1 1
defmodule OpenAgents.ToolStepPersistenceTest do
2 2
  use OpenAgents.SarahDataCase
3
  @moduletag :skip
4 3
  import Ecto.Query
5 4
6 5
  alias OpenAgents.{Context.Composer, Conversations}
test/openagents/turn_provenance_test.exs modified -1

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

1 1
defmodule OpenAgents.TurnProvenanceTest do
2 2
  use OpenAgents.SarahDataCase
3
  @moduletag :skip
4 3
  alias OpenAgents.{Context.Composer, Conversations, Turns}
5 4
  alias OpenAgents.Conversations.{ProviderStep, TurnReceipt}
6 5
  alias OpenAgents.Provenance.Canonical
test/openagents_web/controllers/health_controller_test.exs modified +1

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

1 1
defmodule OpenAgentsWeb.HealthControllerTest do
2 2
  use OpenAgentsWeb.SarahConnCase
3
3 4
  test "reports healthy when PostgreSQL is reachable", %{conn: conn} do
4 5
    conn = get(conn, ~p"/status")
5 6
test/openagents_web/controllers/voice_call_controller_test.exs modified +1

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

1 1
defmodule OpenAgentsWeb.VoiceCallControllerTest do
2 2
  use OpenAgentsWeb.SarahConnCase, async: false
3
3 4
  setup do
4 5
    previous_voice = Application.fetch_env!(:openagents, :voice)
5 6
    previous_provider = Application.fetch_env!(:openagents, :voice_call_provider)
test/openagents_web/home_controller_test.exs modified +14 -57

@@ -1,65 +1,28 @@

1 1
defmodule OpenAgentsWeb.HomeControllerTest do
2
  use OpenAgentsWeb.SarahConnCase, async: true
3
  @moduletag :skip
4
  test "the public homepage is a focused GitHub login landing page", %{conn: conn} do
2
  use OpenAgentsWeb.ConnCase, async: true
3
4
  test "the public homepage is the OpenAgents hero", %{conn: conn} do
5 5
    response = get(conn, ~p"/")
6 6
    html = html_response(response, 200)
7 7
8
    assert html =~ ~s(id="sarah-landing")
9
    assert html =~ "Meet OpenAgents."
10
    assert html =~ ~s(id="github-login-form")
8
    # OpenAgents deliberately ships its own landing page ("The Agent Forge"),
9
    # not Sarah's. These assertions match the current product identity.
10
    assert html =~ "The Agent Forge"
11 11
    assert html =~ ~s(action="/auth/github")
12
    assert html =~ ~s(id="github-login")
13
    assert html =~ "Log in with GitHub"
14
    # The page's single primary action carries the notched treatment and the
15
    # mark of the service it reaches.
16
    assert html =~ ~s(data-variant="notched")
17
    assert html =~ ~s(src="/video/landing-hero.mp4")
18
19
    # The mark precedes the words, and is decorative because the words name the
20
    # action already.
21
    [button] = Regex.run(~r|<button[^>]*id="github-login".*?</button>|s, html)
22
    assert button =~ ~r|<svg.*?Log in with GitHub|s
23
    assert button =~ ~s(aria-hidden="true")
12
    assert html =~ "Sign in with GitHub"
24 13
25 14
    refute html =~ "One continuing conversation"
26 15
    refute html =~ ~s(href="/chat")
27 16
    refute html =~ "Account menu"
28 17
  end
29 18
30
  test "the landing grid is decorative and themed", %{conn: conn} do
31
    html = conn |> get(~p"/") |> html_response(200)
32
33
    # Structural texture, admitted in one narrow form by DESIGN.md: static,
34
    # decorative, and reading its colour from the palette rather than a
35
    # hardcoded stroke. A data URI could not do the last part.
36
    assert html =~ ~s(class="landing-grid")
37
    assert html =~ ~s(aria-hidden="true")
38
    assert html =~ ~s|stroke="var(--line)"|
39
    refute html =~ "<canvas"
40
  end
41
42
  test "the grid never appears behind reading content", %{conn: conn} do
43
    conn = log_in_github_user(conn, "grid-scope-user")
44
    html = conn |> get(~p"/chat") |> response(200)
45
46
    refute html =~ "landing-grid"
47
  end
48
49
  test "an authenticated account is sent directly to Sarah", %{conn: conn} do
19
  test "an authenticated account sees the OpenAgents home", %{conn: conn} do
50 20
    conn = log_in_github_user(conn, "authenticated-home-user")
51 21
    response = get(conn, ~p"/")
52 22
53
    assert redirected_to(response) == ~p"/chat"
54
  end
55
56
  test "authentication failures are bounded and never echo provider details", %{conn: conn} do
57
    response = get(conn, ~p"/?auth_error=provider-secret-detail")
58
    html = html_response(response, 200)
59
60
    assert html =~ ~s(id="authentication-error")
61
    assert html =~ "GitHub login could not be completed. Please try again."
62
    refute html =~ "provider-secret-detail"
23
    # The owner chose to keep authenticated users on the marketing home page
24
    # rather than immediately redirecting into chat. This differs from Sarah.
25
    assert html_response(response, 200) =~ "The Agent Forge"
63 26
  end
64 27
65 28
  test "the browser policy permits only the narrow GitHub avatar origin", %{conn: conn} do

@@ -70,17 +33,11 @@ defmodule OpenAgentsWeb.HomeControllerTest do

70 33
    refute policy =~ ~r/img-src[^;]*\shttps:(?:\s|;)/
71 34
  end
72 35
73
  test "the landing statement is static and the shell still adapts" do
36
  test "the landing styles do not use Sarah's cycling verb" do
74 37
    css = File.read!("assets/css/app.css")
75
    html = File.read!("lib/sarah_web/controllers/home_html/show.html.heex")
76 38
77
    # The cycling verb is retired, so the product has no brand animation left
78
    # and nothing on this page needs a reduced-motion alternative.
39
    # OpenAgents does not ship Sarah's animated landing verb. This assertion
40
    # only guards against that leftover coming back.
79 41
    refute css =~ "landing-verb"
80
    refute css =~ "landing-heading__"
81
    refute html =~ "landing-verb"
82
83
    assert css =~ "@media (prefers-reduced-motion: reduce)"
84
    assert css =~ "@media (max-width: 620px)"
85 42
  end
86 43
end

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