Add changelog redaction and transparency tier tests

53cc2908226a · AtlantisPleb · · parent bae8efe6c7e8

Add changelog redaction and transparency tier 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 lib/openagents/changelog.ex
  • modified lib/openagents/transparency.ex
  • modified priv/repo/migrations/20260823150050_create_artifact_links.exs
  • added test/openagents/transparency/artifact_link_test.exs
  • added test/openagents/transparency_test.exs

Diff

5 files changed, +148 -2

lib/openagents/changelog.ex modified +18

@@ -18,6 +18,7 @@ defmodule OpenAgents.Changelog do

18 18
  alias OpenAgents.Changelog.Entry
19 19
  alias OpenAgents.Forge.{BuildReceipt, DeployReceipt, PushReceipt, Visibility}
20 20
  alias OpenAgents.Repo
21
  alias OpenAgents.Transparency
21 22
22 23
  @schema_version "openagents.changelog.v1"
23 24
  @cache_key {__MODULE__, :cache}

@@ -65,6 +66,23 @@ defmodule OpenAgents.Changelog do

65 66
    |> Repo.insert(on_conflict: :nothing, conflict_target: [:repo, :sha, :source])
66 67
  end
67 68
69
  @doc """
70
  Returns `entry` with `trace_ref`, `trace_digest`, and `detail` redacted
71
  according to the effective transparency tier for `viewer`.
72
73
  `viewer` is a tier atom/string, a map with a `:tier` field, or `nil`.
74
  """
75
  def redact_for_viewer(%Entry{} = entry, viewer) do
76
    tier = entry.transparency_tier
77
78
    %{
79
      entry
80
      | trace_ref: if(Transparency.allows?(tier, :metadata, viewer), do: entry.trace_ref, else: nil),
81
        trace_digest: if(Transparency.allows?(tier, :metadata, viewer), do: entry.trace_digest, else: nil),
82
        detail: if(Transparency.allows?(tier, :content, viewer), do: entry.detail, else: %{})
83
    }
84
  end
85
68 86
  # ── assembly ─────────────────────────────────────────────────────────────
69 87
70 88
  defp cached(repo) do
lib/openagents/transparency.ex modified +2 -1

@@ -84,7 +84,8 @@ defmodule OpenAgents.Transparency do

84 84
  defp tier_atom(_), do: :dark
85 85
86 86
  defp viewer_tier(%{tier: tier}), do: tier_atom(tier)
87
  defp viewer_tier(tier) when is_atom(tier) or is_binary(tier), do: tier_atom(tier)
87
  defp viewer_tier(tier) when is_binary(tier), do: tier_atom(tier)
88
  defp viewer_tier(tier) when is_atom(tier) and not is_nil(tier), do: tier_atom(tier)
88 89
  defp viewer_tier(_), do: nil
89 90
90 91
  defp clamp(tier, viewer) do
test/openagents/transparency_test.exs added +92

@@ -0,0 +1,92 @@

1
defmodule OpenAgents.TransparencyTest do
2
  @moduledoc """
3
  Tests for `OpenAgents.Transparency` tier logic and revocation.
4
  """
5
6
  use OpenAgents.DataCase, async: true
7
8
  alias OpenAgents.Transparency.ArtifactLink
9
10
  import OpenAgents.Transparency,
11
    only: [allows?: 3, effective_tier: 2, tier_atoms: 0, revoke: 3]
12
13
  describe "tier_atoms/0" do
14
    test "returns the tiers from least to most transparent" do
15
      assert tier_atoms() == [:dark, :pulse, :ledger, :glass]
16
    end
17
  end
18
19
  describe "effective_tier/2" do
20
    test "resolves tier atoms and strings" do
21
      assert effective_tier(:dark, nil) == :dark
22
      assert effective_tier("pulse", nil) == :pulse
23
      assert effective_tier(:ledger, nil) == :ledger
24
      assert effective_tier("glass", nil) == :glass
25
    end
26
27
    test "clamps to the lower of the artifact tier and the viewer tier" do
28
      assert effective_tier(:glass, "pulse") == :pulse
29
      assert effective_tier(:ledger, :glass) == :ledger
30
      assert effective_tier(:ledger, %{tier: "dark"}) == :dark
31
      assert effective_tier(:glass, %{tier: :pulse}) == :pulse
32
    end
33
34
    test "a revoked artifact link is always dark" do
35
      revoked = %ArtifactLink{tier: "glass", revoked_at: DateTime.utc_now()}
36
37
      assert effective_tier(revoked, :glass) == :dark
38
      assert effective_tier(revoked, nil) == :dark
39
    end
40
41
    test "an unrevoked artifact link is clamped by the viewer" do
42
      link = %ArtifactLink{tier: "glass", revoked_at: nil}
43
44
      assert effective_tier(link, :ledger) == :ledger
45
      assert effective_tier(link, "pulse") == :pulse
46
      assert effective_tier(link, nil) == :glass
47
    end
48
  end
49
50
  describe "allows?/3" do
51
    test "metadata requires at least pulse" do
52
      refute allows?(:dark, :metadata, nil)
53
      assert allows?(:pulse, :metadata, nil)
54
      assert allows?(:ledger, :metadata, nil)
55
      assert allows?(:glass, :metadata, nil)
56
    end
57
58
    test "content requires at least ledger" do
59
      refute allows?(:pulse, :content, nil)
60
      assert allows?(:ledger, :content, nil)
61
      assert allows?(:glass, :content, nil)
62
    end
63
64
    test "full requires glass" do
65
      refute allows?(:ledger, :full, nil)
66
      assert allows?(:glass, :full, nil)
67
    end
68
69
    test "a lower viewer tier restricts the result" do
70
      refute allows?(:glass, :full, :ledger)
71
      refute allows?(:ledger, :content, :pulse)
72
      refute allows?(:glass, :metadata, :dark)
73
    end
74
  end
75
76
  describe "revoke/3" do
77
    test "sets revoked_at and a revocation tombstone" do
78
      link = %ArtifactLink{tier: "ledger"}
79
      revoker_id = Ecto.UUID.generate()
80
81
      changeset = revoke(link, "expired", revoker_id)
82
83
      assert %DateTime{} = get_field(changeset, :revoked_at)
84
85
      assert %{
86
               reason: "expired",
87
               revoked_by_id: ^revoker_id,
88
               revoked_at: _iso
89
             } = get_field(changeset, :revocation_tombstone)
90
    end
91
  end
92
end

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