| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/hostbox" |
| 10 | "github.com/carlosframework/platform/internal/hostclaim" |
| 11 | "github.com/carlosframework/platform/internal/hostwrite" |
| 12 | "github.com/carlosframework/platform/internal/lease/leasetest" |
| 13 | "github.com/carlosframework/platform/internal/registry" |
| 14 | ) |
| 15 | |
| 16 | // The claims-off fallback keeps the legacy refusal even when serving |
| 17 | // sources exist. Claims-on dispatch is covered by the assembly flag matrix. |
| 18 | func TestClaimOffKeepsLegacyRefusalWithRegisteredSource(t *testing.T) { |
| 19 | t.Setenv("CARLOS_HOST_CLAIM", "") |
| 20 | served := 0 |
| 21 | d5 := plan9D5Fixture(t) |
| 22 | sources := d5.owners.Sources |
| 23 | launch := sources.Launch |
| 24 | sources.Launch = func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, error) { |
| 25 | served++ |
| 26 | return launch(ctx, rt) |
| 27 | } |
| 28 | b := leasetest.NewVersionedBucket() |
| 29 | b.AddBucket("data") |
| 30 | out := ClaimOwnerBindings{Data: b, Serving: sources} |
| 31 | bindClaimLegacyOwners(&out) |
| 32 | if out.Inputs == nil || out.AdmissionEvidence == nil || out.PublicationEvidence == nil { |
| 33 | t.Fatal("legacy owners unbound: the moves-off constructor would refuse at startup") |
| 34 | } |
| 35 | if out.Serving.Files == nil || out.Serving.Launch == nil || out.Serving.Markers == nil { |
| 36 | t.Fatal("serving sources lost for the moves-on constructor") |
| 37 | } |
| 38 | var refusal *ClaimDependencyError |
| 39 | f := newRecoveryFixture(t) |
| 40 | c := f.controller("boot-1", "controller-1", 4101, "start-1") |
| 41 | route := registry.Route{Host: recoveryHost, DBPath: "/data/" + recoveryHost + "/data.db"} |
| 42 | // Rebuild the runtime with the constructor's moves-off bindings: the legacy |
| 43 | // Inputs, and the retry digest selected through them. |
| 44 | c.wake.Inputs = out.Inputs |
| 45 | c.wake.Retries.Inputs = func(ctx context.Context, rt registry.Route) (string, error) { |
| 46 | o, err := hostclaim.Read(ctx, f.claims, rt.Host) |
| 47 | if err != nil { |
| 48 | return "", err |
| 49 | } |
| 50 | _, err = out.Inputs.Select(ctx, rt, o, recoveryPolicy) |
| 51 | return "inputs", err |
| 52 | } |
| 53 | c.wake.pending = nil |
| 54 | source := hostbox.WorkSource{Guard: f.guard, Box: "box-1", Region: "ie", Now: func() time.Time { return f.now }, Policy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { return nil }, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }} |
| 55 | writer := hostbox.Writer{Claims: f.claims, Box: "box-1", Region: "ie", AdmissionEvidence: out.AdmissionEvidence} |
| 56 | runtime, err := c.wake.Build(t.Context(), source, writer) |
| 57 | if err != nil { |
| 58 | t.Fatal(err) |
| 59 | } |
| 60 | c.runtime = runtime |
| 61 | before := f.read() |
| 62 | if _, err := out.Inputs.Select(t.Context(), route, before, recoveryPolicy); !errors.As(err, &refusal) { |
| 63 | t.Fatalf("moves-off select = %v, want main's typed refusal", err) |
| 64 | } |
| 65 | for name, call := range map[string]func() error{ |
| 66 | "materialize": func() error { return out.Inputs.Materialize(t.Context(), before, ClaimLaunchInputs{}, nil) }, |
| 67 | "empty": func() error { return out.Inputs.EmptyAuthority(t.Context(), before, ClaimLaunchInputs{}) }, |
| 68 | "publish": func() error { return out.Inputs.Publish(t.Context(), before, ClaimLaunchInputs{}) }, |
| 69 | "admission": func() error { return out.AdmissionEvidence(t.Context(), before, hostclaim.Admission{}, nil) }, |
| 70 | "publication": func() error { return out.PublicationEvidence(t.Context(), before) }, |
| 71 | } { |
| 72 | if err := call(); !errors.As(err, &refusal) { |
| 73 | t.Fatalf("moves-off %s = %v, want main's typed refusal", name, err) |
| 74 | } |
| 75 | } |
| 76 | // Admit runs with the lease the fixture stands in for; the refusal must |
| 77 | // come before any session, stop or retry reservation. |
| 78 | live, cancel := context.WithCancel(context.Background()) |
| 79 | t.Cleanup(cancel) |
| 80 | done := make(chan struct{}) |
| 81 | go func() { <-live.Done(); close(done) }() |
| 82 | c.wake.mu.Lock() |
| 83 | c.wake.pending[recoveryHost] = &claimWakeAttempt{observed: before, held: recoveryHeld{}, ctx: live, cancel: cancel, done: done} |
| 84 | c.wake.mu.Unlock() |
| 85 | if _, err := c.runtime.Admit(t.Context(), route, before, recoveryPolicy); !errors.As(err, &refusal) { |
| 86 | t.Fatalf("moves-off admit = %v, want main's typed refusal", err) |
| 87 | } |
| 88 | after := f.read() |
| 89 | if after.ETag != before.ETag || after.Claim.Holder.Session != before.Claim.Holder.Session || after.Claim.Holder.Generation != before.Claim.Holder.Generation || after.Claim.Holder.WakeRetry != nil { |
| 90 | t.Fatalf("moves-off wake consumed a session: %+v", after.Claim.Holder) |
| 91 | } |
| 92 | if len(f.requests) != 0 { |
| 93 | t.Fatalf("moves-off wake stopped something: %+v", f.requests) |
| 94 | } |
| 95 | if served != 0 || len(d5.files) != 0 { |
| 96 | t.Fatalf("moves-off wake reached the serving sources: launch=%d files=%v", served, d5.files) |
| 97 | } |
| 98 | } |
| 99 | |