package host import ( "context" "errors" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/lease/leasetest" "github.com/carlosframework/platform/internal/registry" ) // The claims-off fallback keeps the legacy refusal even when serving // sources exist. Claims-on dispatch is covered by the assembly flag matrix. func TestClaimOffKeepsLegacyRefusalWithRegisteredSource(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "") served := 0 d5 := plan9D5Fixture(t) sources := d5.owners.Sources launch := sources.Launch sources.Launch = func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, error) { served++ return launch(ctx, rt) } b := leasetest.NewVersionedBucket() b.AddBucket("data") out := ClaimOwnerBindings{Data: b, Serving: sources} bindClaimLegacyOwners(&out) if out.Inputs == nil || out.AdmissionEvidence == nil || out.PublicationEvidence == nil { t.Fatal("legacy owners unbound: the moves-off constructor would refuse at startup") } if out.Serving.Files == nil || out.Serving.Launch == nil || out.Serving.Markers == nil { t.Fatal("serving sources lost for the moves-on constructor") } var refusal *ClaimDependencyError f := newRecoveryFixture(t) c := f.controller("boot-1", "controller-1", 4101, "start-1") route := registry.Route{Host: recoveryHost, DBPath: "/data/" + recoveryHost + "/data.db"} // Rebuild the runtime with the constructor's moves-off bindings: the legacy // Inputs, and the retry digest selected through them. c.wake.Inputs = out.Inputs c.wake.Retries.Inputs = func(ctx context.Context, rt registry.Route) (string, error) { o, err := hostclaim.Read(ctx, f.claims, rt.Host) if err != nil { return "", err } _, err = out.Inputs.Select(ctx, rt, o, recoveryPolicy) return "inputs", err } c.wake.pending = nil 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 }} writer := hostbox.Writer{Claims: f.claims, Box: "box-1", Region: "ie", AdmissionEvidence: out.AdmissionEvidence} runtime, err := c.wake.Build(t.Context(), source, writer) if err != nil { t.Fatal(err) } c.runtime = runtime before := f.read() if _, err := out.Inputs.Select(t.Context(), route, before, recoveryPolicy); !errors.As(err, &refusal) { t.Fatalf("moves-off select = %v, want main's typed refusal", err) } for name, call := range map[string]func() error{ "materialize": func() error { return out.Inputs.Materialize(t.Context(), before, ClaimLaunchInputs{}, nil) }, "empty": func() error { return out.Inputs.EmptyAuthority(t.Context(), before, ClaimLaunchInputs{}) }, "publish": func() error { return out.Inputs.Publish(t.Context(), before, ClaimLaunchInputs{}) }, "admission": func() error { return out.AdmissionEvidence(t.Context(), before, hostclaim.Admission{}, nil) }, "publication": func() error { return out.PublicationEvidence(t.Context(), before) }, } { if err := call(); !errors.As(err, &refusal) { t.Fatalf("moves-off %s = %v, want main's typed refusal", name, err) } } // Admit runs with the lease the fixture stands in for; the refusal must // come before any session, stop or retry reservation. live, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) done := make(chan struct{}) go func() { <-live.Done(); close(done) }() c.wake.mu.Lock() c.wake.pending[recoveryHost] = &claimWakeAttempt{observed: before, held: recoveryHeld{}, ctx: live, cancel: cancel, done: done} c.wake.mu.Unlock() if _, err := c.runtime.Admit(t.Context(), route, before, recoveryPolicy); !errors.As(err, &refusal) { t.Fatalf("moves-off admit = %v, want main's typed refusal", err) } after := f.read() 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 { t.Fatalf("moves-off wake consumed a session: %+v", after.Claim.Holder) } if len(f.requests) != 0 { t.Fatalf("moves-off wake stopped something: %+v", f.requests) } if served != 0 || len(d5.files) != 0 { t.Fatalf("moves-off wake reached the serving sources: launch=%d files=%v", served, d5.files) } }