| 1 |
1
|
|
defmodule OpenAgents.Forge.TargetsTest do
|
|
2
|
+ |
@moduledoc """
|
|
3
|
+ |
The two surfaces of `OpenAgents.Forge.Targets` that the end-to-end
|
|
4
|
+ |
lifecycle test (`OpenAgents.Forge.SarahTargetsTest`, which drives a real
|
|
5
|
+ |
bare repo) does not reach: the injectable `commit_store`, and the bounded
|
|
6
|
+ |
`details` map.
|
|
7
|
+ |
|
|
8
|
+ |
Promotability and the transition table are deliberately NOT re-asserted
|
|
9
|
+ |
here — they have one contract and one home. This file used to promote
|
|
10
|
+ |
never-pushed SHAs like `"abc123"` through a test-environment bypass, which
|
|
11
|
+ |
contradicted that contract ("only pushed commits are ever promotable") and
|
|
12
|
+ |
meant the precondition was never actually exercised anywhere.
|
|
13
|
+ |
"""
|
|
14
|
+ |
|
| 2 |
15
|
|
use OpenAgents.DataCase, async: false
|
| 3 |
16
|
|
|
| 4 |
17
|
|
alias OpenAgents.Forge.Target
|
| 5 |
18
|
|
alias OpenAgents.Forge.Targets
|
| 6 |
19
|
|
|
| 7 |
|
- |
test "promote a target" do
|
| 8 |
|
- |
assert {:ok, %Target{} = target} =
|
| 9 |
|
- |
Targets.promote("OpenAgents/openagents.com", "abc123", "operator-1")
|
| 10 |
|
- |
|
| 11 |
|
- |
assert target.repo == "OpenAgents/openagents.com"
|
| 12 |
|
- |
assert target.sha == "abc123"
|
| 13 |
|
- |
assert target.promoted_by == "operator-1"
|
| 14 |
|
- |
assert target.status == "promoted"
|
| 15 |
|
- |
end
|
|
20
|
+ |
@repo "OpenAgents/openagents.com"
|
| 16 |
21
|
|
|
| 17 |
|
- |
test "refuse promotion of an unknown SHA" do
|
| 18 |
|
- |
bad_store = fn _repo, _sha -> :error end
|
|
22
|
+ |
# A SHA that is well-formed but not in any repo, so only the injected
|
|
23
|
+ |
# store decides whether it is promotable.
|
|
24
|
+ |
defp sha, do: 20 |> :crypto.strong_rand_bytes() |> Base.encode16(case: :lower)
|
| 19 |
25
|
|
|
| 20 |
|
- |
assert {:error, :unknown_sha} =
|
| 21 |
|
- |
Targets.promote(
|
| 22 |
|
- |
"OpenAgents/openagents.com",
|
| 23 |
|
- |
"badsha",
|
| 24 |
|
- |
"operator-1",
|
| 25 |
|
- |
commit_store: bad_store
|
| 26 |
|
- |
)
|
|
26
|
+ |
defp promote(attrs \\ []) do
|
|
27
|
+ |
Targets.promote(
|
|
28
|
+ |
Keyword.get(attrs, :repo, @repo),
|
|
29
|
+ |
Keyword.get(attrs, :sha, sha()),
|
|
30
|
+ |
Keyword.get(attrs, :operator, "operator-1"),
|
|
31
|
+ |
Keyword.take(attrs, [:commit_store, :details])
|
|
32
|
+ |
|> Keyword.put_new(:commit_store, fn _repo, _sha -> :ok end)
|
|
33
|
+ |
)
|
| 27 |
34
|
|
end
|
| 28 |
35
|
|
|
| 29 |
|
- |
test "complete lifecycle from promoted to live" do
|
| 30 |
|
- |
{:ok, target} = Targets.promote("OpenAgents/openagents.com", "abc123", "operator-1")
|
|
36
|
+ |
test "an accepting commit store promotes the commit" do
|
|
37
|
+ |
sha = sha()
|
| 31 |
38
|
|
|
| 32 |
|
- |
for status <- ["building", "built", "deploying", "live"] do
|
| 33 |
|
- |
assert {:ok, %Target{} = updated} = Targets.transition(target.id, status)
|
| 34 |
|
- |
assert updated.status == status
|
| 35 |
|
- |
end
|
|
39
|
+ |
assert {:ok, %Target{} = target} = promote(sha: sha)
|
|
40
|
+ |
assert target.repo == @repo
|
|
41
|
+ |
assert target.sha == sha
|
|
42
|
+ |
assert target.promoted_by == "operator-1"
|
|
43
|
+ |
assert target.status == "promoted"
|
| 36 |
44
|
|
end
|
| 37 |
45
|
|
|
| 38 |
|
- |
test "refuse invalid transitions" do
|
| 39 |
|
- |
{:ok, target} = Targets.promote("OpenAgents/openagents.com", "abc123", "operator-1")
|
| 40 |
|
- |
|
| 41 |
|
- |
assert {:error, {:invalid_transition, "promoted", "live"}} =
|
| 42 |
|
- |
Targets.transition(target.id, "live")
|
|
46
|
+ |
test "a refusing commit store refuses the promotion" do
|
|
47
|
+ |
assert {:error, :unknown_sha} = promote(commit_store: fn _repo, _sha -> :error end)
|
| 43 |
48
|
|
end
|
| 44 |
49
|
|
|
| 45 |
|
- |
test "refuse transitions out of a terminal state" do
|
| 46 |
|
- |
{:ok, target} = Targets.promote("OpenAgents/openagents.com", "abc123", "operator-1")
|
| 47 |
|
- |
|
| 48 |
|
- |
assert {:ok, %Target{} = built} = Targets.transition(target.id, "building")
|
| 49 |
|
- |
assert {:ok, %Target{} = built} = Targets.transition(built.id, "built")
|
| 50 |
|
- |
assert {:ok, %Target{} = deploying} = Targets.transition(built.id, "deploying")
|
| 51 |
|
- |
assert {:ok, %Target{} = live} = Targets.transition(deploying.id, "live")
|
| 52 |
|
- |
|
| 53 |
|
- |
assert {:error, {:terminal, "live"}} = Targets.transition(live.id, "reverted")
|
|
50
|
+ |
test "a commit store may name its own reason" do
|
|
51
|
+ |
store = fn _repo, _sha -> {:error, :mirror_behind} end
|
|
52
|
+ |
assert {:error, :mirror_behind} = promote(commit_store: store)
|
| 54 |
53
|
|
end
|
| 55 |
54
|
|
|
| 56 |
|
- |
test "pinning back to an older SHA creates a new latest target" do
|
| 57 |
|
- |
repo = "OpenAgents/openagents.com"
|
| 58 |
|
- |
|
| 59 |
|
- |
{:ok, first} = Targets.promote(repo, "sha-1", "operator-1")
|
| 60 |
|
- |
{:ok, second} = Targets.promote(repo, "sha-2", "operator-1")
|
| 61 |
|
- |
assert Targets.latest(repo).id == second.id
|
|
55
|
+ |
test "a malformed SHA is refused before the store is consulted" do
|
|
56
|
+ |
store = fn _repo, _sha -> flunk("commit store must not be consulted") end
|
| 62 |
57
|
|
|
| 63 |
|
- |
{:ok, third} = Targets.promote(repo, "sha-1", "operator-1")
|
| 64 |
|
- |
assert Targets.latest(repo).id == third.id
|
| 65 |
|
- |
assert third.sha == "sha-1"
|
| 66 |
|
- |
refute third.id == first.id
|
|
58
|
+ |
assert {:error, :invalid_sha} =
|
|
59
|
+ |
Targets.promote(@repo, "not-a-sha!", "operator-1", commit_store: store)
|
| 67 |
60
|
|
end
|
| 68 |
61
|
|
|
| 69 |
|
- |
test "bound details to 100 keys and 32KB" do
|
|
62
|
+ |
test "details are bounded to 100 keys and 32KB per value" do
|
| 70 |
63
|
|
too_many = Map.new(0..100, fn i -> {"key#{i}", "value"} end)
|
| 71 |
64
|
|
|
| 72 |
|
- |
assert {:error, {:invalid, %Ecto.Changeset{} = changeset}} =
|
| 73 |
|
- |
Targets.promote("OpenAgents/openagents.com", "abc123", "operator-1",
|
| 74 |
|
- |
details: too_many
|
| 75 |
|
- |
)
|
| 76 |
|
- |
|
|
65
|
+ |
assert {:error, {:invalid, %Ecto.Changeset{} = changeset}} = promote(details: too_many)
|
| 77 |
66
|
|
assert "exceeds the 100-key bound" in errors_on(changeset).details
|
| 78 |
67
|
|
|
| 79 |
|
- |
huge_string = String.duplicate("x", 40_000)
|
|
68
|
+ |
huge = String.duplicate("x", 40_000)
|
| 80 |
69
|
|
|
| 81 |
70
|
|
assert {:error, {:invalid, %Ecto.Changeset{} = changeset}} =
|
| 82 |
|
- |
Targets.promote("OpenAgents/openagents.com", "def456", "operator-1",
|
| 83 |
|
- |
details: %{data: huge_string}
|
| 84 |
|
- |
)
|
|
71
|
+ |
promote(details: %{data: huge})
|
| 85 |
72
|
|
|
| 86 |
73
|
|
assert "exceeds the 32KB bound" in errors_on(changeset).details
|
| 87 |
74
|
|
end
|
|
75
|
+ |
|
|
76
|
+ |
test "latest/1 and transition/2 are the current/advance aliases" do
|
|
77
|
+ |
{:ok, first} = promote()
|
|
78
|
+ |
{:ok, second} = promote()
|
|
79
|
+ |
|
|
80
|
+ |
assert Targets.latest(@repo).id == second.id
|
|
81
|
+ |
assert {:ok, %Target{status: "building"}} = Targets.transition(first.id, "building")
|
|
82
|
+ |
|
|
83
|
+ |
assert {:error, {:invalid_transition, "promoted", "live"}} =
|
|
84
|
+ |
Targets.transition(second.id, "live")
|
|
85
|
+ |
end
|
| 88 |
86
|
|
end
|