diff --git a/internal/selfcompose/entries_test.go b/internal/selfcompose/entries_test.go index b7fb2ac1..24d32128 100644 --- a/internal/selfcompose/entries_test.go +++ b/internal/selfcompose/entries_test.go @@ -5,8 +5,10 @@ import ( "encoding/json" "errors" "fmt" + "runtime" "strings" "sync" + "sync/atomic" "testing" "time" @@ -128,6 +130,32 @@ func composeFromEntries(t *testing.T) { if !nf || nfOther { t.Errorf("entry-not-found told %v for the ghost and %v for the entry held", nf, nfOther) } + // An entry asked again by hand is asked again in the shadow's cache too (lead answer A-319): the shadow holds the ghost as not + // found, the half then holds it, and the node's own composer asked again takes it in on the shadow as well. + var late atomic.Bool + shadowEnv := b.env() + shadowEnv.Entry = EntryCall(func(ctx context.Context, seat, verb string, args any) (json.RawMessage, error) { + if in := args.(meshcatalog.EntryInput); in.Fingerprint == ghost && late.Load() { + return json.Marshal(meshcatalog.EntryAnswer{Entry: meshcatalog.CatalogEntry{Module: "ghost", Fp: ghost}}) + } + return ask(ctx, seat, verb, args) + }) + shadow := NewShadow(shadowEnv, nil) + shadow.c.entries.Need(ctx, ghost) + for deadline := time.Now().Add(10 * time.Second); shadow.c.entries.Rows() == nil || shadow.c.entries.Rows()[0].State != meshcatalogclient.EntryNotFound; { + if time.Now().After(deadline) { + t.Fatal("the shadow did not hold the ghost as not found") + } + runtime.Gosched() + } + late.Store(true) + c.AlsoEntryAgain = shadow.EntryAgain + if err := c.EntryAgain(ctx, ghost); err != nil { + t.Fatal(err) + } + if _, state := shadow.c.entries.Need(ctx, ghost); state != meshcatalogclient.EntryAnswered { + t.Errorf("the shadow holds the ghost's entry as %s after entry-again, want answered", state) + } // Each entry and want it composes from is kept in the composition state before the composition that uses them. state.mu.Lock() _, hasEntry := state.kv[entryKey(fp)]