carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package edgesfile
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "log"
8 "os"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/status"
15)
16
17// Stage B Task 10: the console's automatic-eligibility read and the box
18// agent's relocation chooser read the same live snapshot. Every listed
19// edges file is read, each box's liveness is its status snapshot's
20// freshness, a stale snapshot is a retired box (complete evidence, not
21// fresh), and a file with no snapshot at all, or an unreadable file, makes
22// the evidence incomplete. BoxChoice carries every field the predicate
23// reads.
24func TestLiveSnapshotJoinsEdgesFilesWithLiveness(t *testing.T) {
25 ctx := context.Background()
26 now := time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC)
27 s := release.NewDirStore(t.TempDir())
28 put := func(box, body string) {
29 t.Helper()
30 if _, err := s.Put(ctx, Key(box), []byte(body), ""); err != nil {
31 t.Fatal(err)
32 }
33 }
34 snapshot := func(box string, at time.Time) {
35 t.Helper()
36 if err := status.WriteBox(ctx, s, status.Box{Label: box, GeneratedAt: at}); err != nil {
37 t.Fatal(err)
38 }
39 }
40 put("box-a", `{"v":1,"box":"box-a","box_id":"i-a","eip":"203.0.113.1","region":"eu-west-1","placement":"ie","hibernation_configured":true,"secrets_key_fp":"fp","capabilities":["region-ha-1"],"headroom":{"mem_free_mib":512,"disk_free_mib":2048,"awake":3},"hosts":{}}`)
41 put("box-b", `{"v":1,"box":"box-b","eip":"203.0.113.2","region":"eu-west-1","placement":"ie","hosts":{}}`)
42 snapshot("box-a", now.Add(-time.Second))
43 snapshot("box-b", now.Add(-24*time.Hour))
44
45 live, err := ReadLiveSnapshot(ctx, s, now)
46 if err != nil {
47 t.Fatal(err)
48 }
49 if !live.EvidenceComplete() || len(live.Files) != 2 {
50 t.Fatalf("snapshot %+v", live)
51 }
52 fresh := map[string]bool{}
53 for _, f := range live.Files {
54 fresh[f.Box] = f.Fresh
55 }
56 if !fresh["box-a"] || fresh["box-b"] {
57 t.Fatalf("liveness %v: box-a fresh, box-b retired", fresh)
58 }
59 choice, err := LiveSnapshot(ctx, s, now)
60 if err != nil || !choice.Complete || len(choice.Boxes) != 2 {
61 t.Fatalf("boxchoice %+v %v", choice, err)
62 }
63 for _, b := range choice.Boxes {
64 if b.Box == "box-a" && (b.BoxID != "i-a" || b.SecretsKeyFP != "fp" || !b.Fresh || b.Headroom == nil || b.Headroom.Awake != 3 || len(b.Capabilities) != 1 || b.HibernationConfigured == nil || !*b.HibernationConfigured) {
65 t.Fatalf("box-a lost a field: %+v", b)
66 }
67 }
68
69 // A box with an edges file and no status snapshot: unknown liveness.
70 put("box-c", `{"v":1,"box":"box-c","region":"eu-west-1","placement":"ie","hosts":{}}`)
71 if live, err = ReadLiveSnapshot(ctx, s, now); err != nil || live.EvidenceComplete() {
72 t.Fatalf("a file with no snapshot left the evidence complete: %+v %v", live, err)
73 }
74 // An unreadable file: incomplete, the others still read.
75 put("box-d", `not json`)
76 plain, err := ReadSnapshot(ctx, s)
77 if err != nil || plain.Complete || len(plain.Files) != 3 {
78 t.Fatalf("an unreadable file: %+v %v", plain, err)
79 }
80}
81
82// Task 10 review M5: no edges file at all is not complete evidence by
83// itself. A fresh box with no edges file makes the evidence incomplete
84// (ruling (A)), exactly as it does beside other files; with no file and
85// no fresh box the evidence is complete and empty.
86func TestLiveSnapshotWithNoEdgesFilesStillJoinsLiveness(t *testing.T) {
87 ctx := context.Background()
88 now := time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC)
89 s := release.NewDirStore(t.TempDir())
90 live, err := ReadLiveSnapshot(ctx, s, now)
91 if err != nil || !live.EvidenceComplete() || len(live.Files) != 0 {
92 t.Fatalf("an empty deployment: %+v %v", live, err)
93 }
94 if err := status.WriteBox(ctx, s, status.Box{Label: "box-a", GeneratedAt: now.Add(-time.Second)}); err != nil {
95 t.Fatal(err)
96 }
97 if live, err = ReadLiveSnapshot(ctx, s, now); err != nil || live.EvidenceComplete() {
98 t.Fatalf("a fresh box with no edges file left the evidence complete: %+v %v", live, err)
99 }
100 if choice, err := LiveSnapshot(ctx, s, now); err != nil || choice.Complete {
101 t.Fatalf("boxchoice complete over a fresh box with no edges file: %+v %v", choice, err)
102 }
103}
104
105// The load failure is said once per process: the throttle the console
106// and the pass share lives here (Task 10 review M10).
107func TestReadLiveSnapshotSaysALoadFailureOnce(t *testing.T) {
108 ctx := context.Background()
109 s := release.NewDirStore(t.TempDir())
110 if _, err := s.Put(ctx, Key("box-a"), []byte(`{"v":1,"box":"box-a","region":"eu-west-1","hosts":{}}`), ""); err != nil {
111 t.Fatal(err)
112 }
113 var buf bytes.Buffer
114 log.SetOutput(&buf)
115 t.Cleanup(func() { log.SetOutput(os.Stderr) })
116 livenessSaid.Delete("load")
117 for range 2 {
118 live, err := ReadLiveSnapshot(ctx, failingStatusList{s}, time.Now())
119 if err != nil || live.EvidenceComplete() {
120 t.Fatalf("snapshots unloadable: %+v %v", live, err)
121 }
122 }
123 if n := strings.Count(buf.String(), "could not load box snapshots"); n != 1 {
124 t.Fatalf("said %d times, want once:\n%s", n, buf.String())
125 }
126}
127
128// failingStatusList fails the status half of the join and serves the rest.
129type failingStatusList struct{ release.Store }
130
131func (f failingStatusList) List(ctx context.Context, prefix string) ([]string, error) {
132 if strings.HasPrefix(prefix, "status/") {
133 return nil, errors.New("status bucket unreachable")
134 }
135 return f.Store.List(ctx, prefix)
136}
137