package gio import ( "context" "errors" "image" "strings" "testing" "time" "softbox.local/core/application" ) func TestIconEventRelayAppliesOnlyDuringUIDrain(t *testing.T) { reference := testIconReference("11") shell := NewAppShell("Test", application.CatalogListItem{ ID: "app-one", Name: "One", IconRef: reference, }) identity := testIconIdentity(t, "request-one", "app-one", reference, 96) if err := shell.ExpectIcon(identity); err != nil { t.Fatal(err) } ready, err := application.NewIconReadyEvent( identity, image.NewNRGBA(image.Rect(0, 0, 24, 24)), ) if err != nil { t.Fatal(err) } relay, err := application.NewEventRelay(1) if err != nil { t.Fatal(err) } submitted := make(chan error, 1) go func() { submitted <- relay.Submit(context.Background(), ready) }() if err := waitIconSubmit(submitted); err != nil { t.Fatalf("Submit() error = %v", err) } if _, exists := shell.icons["app-one"]; exists { t.Fatal("background relay changed shell before UI drain") } if err := relay.Drain(shell.ApplyEvent); err != nil { t.Fatalf("Drain() error = %v", err) } icon, exists := shell.icons["app-one"] if !exists || icon.Size() != image.Pt(24, 24) { t.Fatalf("applied icon = (%t, %v)", exists, icon.Size()) } } func TestAppShellAcceptsOnlyLatestIconRequest(t *testing.T) { reference := testIconReference("22") shell := NewAppShell("Test", application.CatalogListItem{ ID: "app-one", Name: "One", IconRef: reference, }) oldIdentity := testIconIdentity(t, "request-old", "app-one", reference, 96) newIdentity := testIconIdentity(t, "request-new", "app-one", reference, 96) if err := shell.ExpectIcon(oldIdentity); err != nil { t.Fatal(err) } if err := shell.ExpectIcon(newIdentity); err != nil { t.Fatal(err) } oldReady, err := application.NewIconReadyEvent( oldIdentity, image.NewNRGBA(image.Rect(0, 0, 12, 12)), ) if err != nil { t.Fatal(err) } if err := shell.ApplyEvent(oldReady); err != nil { t.Fatal(err) } if _, exists := shell.icons["app-one"]; exists { t.Fatal("stale request inserted an icon") } newReady, err := application.NewIconReadyEvent( newIdentity, image.NewNRGBA(image.Rect(0, 0, 30, 30)), ) if err != nil { t.Fatal(err) } if err := shell.ApplyEvent(newReady); err != nil { t.Fatal(err) } if got := shell.icons["app-one"].Size(); got != image.Pt(30, 30) { t.Fatalf("latest icon size = %v", got) } retryIdentity := testIconIdentity(t, "request-retry", "app-one", reference, 96) if err := shell.ExpectIcon(retryIdentity); err != nil { t.Fatal(err) } if _, exists := shell.icons["app-one"]; !exists { t.Fatal("same-resource retry discarded an already valid icon") } failed, err := application.NewIconFailedEvent( retryIdentity, application.IconFailureUnavailable, ) if err != nil { t.Fatal(err) } if err := shell.ApplyEvent(failed); err != nil { t.Fatal(err) } if _, exists := shell.icons["app-one"]; !exists { t.Fatal("matching failure discarded an already valid icon") } if failure, exists := shell.IconFailure("app-one"); !exists || failure != application.IconFailureUnavailable { t.Fatalf("IconFailure() = (%q, %t)", failure, exists) } dpiIdentity := testIconIdentity(t, "request-dpi", "app-one", reference, 144) if err := shell.ExpectIcon(dpiIdentity); err != nil { t.Fatal(err) } if _, exists := shell.icons["app-one"]; exists { t.Fatal("different-DPI request retained an unmatching image") } dpiFailed, err := application.NewIconFailedEvent( dpiIdentity, application.IconFailureUnavailable, ) if err != nil { t.Fatal(err) } if err := shell.ApplyEvent(dpiFailed); err != nil { t.Fatal(err) } if _, exists := shell.icons["app-one"]; exists { t.Fatal("different-DPI failure restored an unmatching image") } } func TestAppShellDropsChangedRemovedAndCanceledIconResults(t *testing.T) { oldReference := testIconReference("33") newReference := testIconReference("44") shell := NewAppShell("Test", application.CatalogListItem{ ID: "app-one", Name: "One", IconRef: oldReference, }) oldIdentity := testIconIdentity(t, "request-old", "app-one", oldReference, 96) if err := shell.ExpectIcon(oldIdentity); err != nil { t.Fatal(err) } oldReady, err := application.NewIconReadyEvent( oldIdentity, image.NewNRGBA(image.Rect(0, 0, 20, 20)), ) if err != nil { t.Fatal(err) } if err := shell.ApplyEvent(oldReady); err != nil { t.Fatal(err) } shell.SetItems([]application.CatalogListItem{{ ID: "app-one", Name: "One", IconRef: newReference, }}) if _, exists := shell.icons["app-one"]; exists { t.Fatal("IconRef change retained the previous image") } if err := shell.ApplyEvent(oldReady); err != nil { t.Fatal(err) } if _, exists := shell.icons["app-one"]; exists { t.Fatal("old IconRef result was reinserted") } newIdentity := testIconIdentity(t, "request-new", "app-one", newReference, 96) if err := shell.ExpectIcon(newIdentity); err != nil { t.Fatal(err) } if !shell.CancelIconRequest("app-one", newIdentity.RequestID) { t.Fatal("CancelIconRequest() did not cancel the latest request") } newReady, err := application.NewIconReadyEvent( newIdentity, image.NewNRGBA(image.Rect(0, 0, 22, 22)), ) if err != nil { t.Fatal(err) } if err := shell.ApplyEvent(newReady); err != nil { t.Fatal(err) } if _, exists := shell.icons["app-one"]; exists { t.Fatal("canceled result was applied") } shell.SetItems(nil) if err := shell.ExpectIcon(newIdentity); !errors.Is(err, ErrIconRequestStale) { t.Fatalf("ExpectIcon(removed app) error = %v", err) } if err := shell.ApplyEvent(newReady); err != nil { t.Fatal(err) } if _, exists := shell.icons["app-one"]; exists { t.Fatal("removed app was reinserted by a late result") } } func TestAppShellRejectsMalformedIconEventAndIgnoresOtherEvents(t *testing.T) { shell := NewAppShell("Test") if err := shell.ApplyEvent(application.Event{Type: application.EventCatalogRefreshed}); err != nil { t.Fatalf("ApplyEvent(non-icon) error = %v", err) } err := shell.ApplyEvent(application.Event{ Type: application.EventIconReady, RequestID: "request", AppID: "app-one", Payload: "wrong", }) if !errors.Is(err, application.ErrInvalidIconEvent) { t.Fatalf("ApplyEvent(invalid payload) error = %v", err) } if len(shell.icons) != 0 { t.Fatal("invalid payload polluted icon state") } } func TestAppShellRendersOnlyUnsafeIconCacheDiagnostic(t *testing.T) { reference := testIconReference("55") item := application.CatalogListItem{ ID: "app-one", Name: "One", Version: "1.0.0", IconRef: reference, } shell := NewAppShell("Test", item) identity := testIconIdentity(t, "request-unsafe", item.ID, reference, 144) if err := shell.ExpectIcon(identity); err != nil { t.Fatal(err) } applyTestIconFailure(t, shell, identity, application.IconFailureUnsafe) shell.model.Select(item.ID) nodes := adapterContractLayout(shell, adapterContractViewport) failure := shell.iconFailures[item.ID] for _, want := range []string{ "图标缓存安全警告", unsafeIconCacheMessage, unsafeIconCacheDiagnostic(failure), } { if !adapterContractHasSemantic(nodes, want) { t.Fatalf("unsafe cache detail is missing semantic text %q", want) } } if diagnostic := unsafeIconCacheDiagnostic(failure); strings.Contains(diagnostic, "sha256:") { t.Fatalf("unsafe cache diagnostic exposed the reference scheme: %q", diagnostic) } unavailable := testIconIdentity(t, "request-unavailable", item.ID, reference, 144) if err := shell.ExpectIcon(unavailable); err != nil { t.Fatal(err) } applyTestIconFailure(t, shell, unavailable, application.IconFailureUnavailable) nodes = adapterContractLayout(shell, adapterContractViewport) if adapterContractHasSemantic(nodes, "图标缓存安全警告") { t.Fatal("ordinary icon failure rendered an unsafe-cache warning") } if code, exists := shell.IconFailure(item.ID); !exists || code != application.IconFailureUnavailable { t.Fatalf("IconFailure() = (%q, %t)", code, exists) } } func TestAppShellRetainsUnsafeDiagnosticOnlyForCurrentResource(t *testing.T) { reference := testIconReference("66") item := application.CatalogListItem{ ID: "app-one", Name: "One", Version: "1.0.0", IconRef: reference, } shell := NewAppShell("Test", item) first := testIconIdentity(t, "request-first", item.ID, reference, 96) if err := shell.ExpectIcon(first); err != nil { t.Fatal(err) } firstFailure := applyTestIconFailure( t, shell, first, application.IconFailureUnsafe, ) if got := shell.iconFailures[item.ID].Identity; got != first { t.Fatalf("stored failure identity = %+v, want %+v", got, first) } shell.SetItems([]application.CatalogListItem{item}) if code, exists := shell.IconFailure(item.ID); !exists || code != application.IconFailureUnsafe { t.Fatal("same-reference snapshot discarded the unsafe diagnostic") } latest := testIconIdentity(t, "request-latest", item.ID, reference, 96) if err := shell.ExpectIcon(latest); err != nil { t.Fatal(err) } if _, exists := shell.IconFailure(item.ID); exists { t.Fatal("new request retained the previous unsafe diagnostic") } if err := shell.ApplyEvent(firstFailure); err != nil { t.Fatal(err) } if _, exists := shell.IconFailure(item.ID); exists { t.Fatal("late failure restored a stale diagnostic") } ready, err := application.NewIconReadyEvent( latest, image.NewNRGBA(image.Rect(0, 0, 16, 16)), ) if err != nil { t.Fatal(err) } if err := shell.ApplyEvent(ready); err != nil { t.Fatal(err) } if _, exists := shell.IconFailure(item.ID); exists { t.Fatal("ready event retained an unsafe diagnostic") } dpiRequest := testIconIdentity(t, "request-dpi", item.ID, reference, 144) if err := shell.ExpectIcon(dpiRequest); err != nil { t.Fatal(err) } applyTestIconFailure(t, shell, dpiRequest, application.IconFailureUnsafe) if got := shell.iconFailures[item.ID].Identity.DPI; got != 144 { t.Fatalf("stored failure DPI = %d, want 144", got) } newReference := testIconReference("77") changed := item changed.IconRef = newReference shell.SetItems([]application.CatalogListItem{changed}) if _, exists := shell.IconFailure(item.ID); exists { t.Fatal("IconRef change retained the unsafe diagnostic") } canceled := testIconIdentity(t, "request-canceled", item.ID, newReference, 96) if err := shell.ExpectIcon(canceled); err != nil { t.Fatal(err) } if !shell.CancelIconRequest(item.ID, canceled.RequestID) { t.Fatal("CancelIconRequest() did not cancel the current request") } applyTestIconFailure(t, shell, canceled, application.IconFailureUnsafe) if _, exists := shell.IconFailure(item.ID); exists { t.Fatal("canceled failure created an unsafe diagnostic") } final := testIconIdentity(t, "request-final", item.ID, newReference, 96) if err := shell.ExpectIcon(final); err != nil { t.Fatal(err) } applyTestIconFailure(t, shell, final, application.IconFailureUnsafe) shell.SetItems(nil) if _, exists := shell.IconFailure(item.ID); exists { t.Fatal("removed app retained the unsafe diagnostic") } } func applyTestIconFailure( t *testing.T, shell *AppShell, identity application.IconEventIdentity, code application.IconFailureCode, ) application.Event { t.Helper() event, err := application.NewIconFailedEvent(identity, code) if err != nil { t.Fatal(err) } if err := shell.ApplyEvent(event); err != nil { t.Fatal(err) } return event } func testIconReference(pair string) string { return "sha256:" + strings.Repeat(pair, 32) } func testIconIdentity( t *testing.T, requestID string, appID string, reference string, dpi int, ) application.IconEventIdentity { t.Helper() identity, err := application.NewIconEventIdentity( requestID, appID, reference, dpi, ) if err != nil { t.Fatal(err) } return identity } func waitIconSubmit(result <-chan error) error { select { case err := <-result: return err case <-time.After(2 * time.Second): return errors.New("timed out waiting for icon relay") } }