Implement unsafe icon cache diagnostics (T-611)

This commit is contained in:
ila
2026-07-18 15:18:29 +08:00
parent 6455fec811
commit 320b83d929
22 changed files with 697 additions and 31 deletions
+11 -2
View File
@@ -9,6 +9,11 @@ import (
var ErrIconRequestStale = errors.New("icon request no longer matches catalog")
type iconFailureState struct {
Identity application.IconEventIdentity
Code application.IconFailureCode
}
// ExpectIcon records the newest request identity on the UI goroutine.
func (shell *AppShell) ExpectIcon(identity application.IconEventIdentity) error {
validated, err := application.NewIconEventIdentity(
@@ -45,6 +50,7 @@ func (shell *AppShell) CancelIconRequest(appID, requestID string) bool {
return false
}
delete(shell.iconRequests, appID)
delete(shell.iconFailures, appID)
return true
}
@@ -73,7 +79,10 @@ func (shell *AppShell) ApplyEvent(event application.Event) error {
if !hasApplied || !sameIconResource(applied, identity) {
shell.ApplyIcon(identity.AppID, nil)
}
shell.iconFailures[identity.AppID] = iconEvent.ErrorCode
shell.iconFailures[identity.AppID] = iconFailureState{
Identity: identity,
Code: iconEvent.ErrorCode,
}
}
return nil
}
@@ -81,7 +90,7 @@ func (shell *AppShell) ApplyEvent(event application.Event) error {
// IconFailure exposes the last failure for diagnostics without raw network data.
func (shell *AppShell) IconFailure(appID string) (application.IconFailureCode, bool) {
failure, exists := shell.iconFailures[appID]
return failure, exists
return failure.Code, exists
}
func canonicalIconReference(reference string) string {
+158
View File
@@ -230,6 +230,164 @@ func TestAppShellRejectsMalformedIconEventAndIgnoresOtherEvents(t *testing.T) {
}
}
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)
}
+5 -4
View File
@@ -33,7 +33,7 @@ type AppShell struct {
iconReferences map[string]string
iconRequests map[string]application.IconEventIdentity
iconApplied map[string]application.IconEventIdentity
iconFailures map[string]application.IconFailureCode
iconFailures map[string]iconFailureState
lastRendered int
detailRendered bool
}
@@ -54,7 +54,7 @@ func NewAppShell(
iconReferences: make(map[string]string),
iconRequests: make(map[string]application.IconEventIdentity),
iconApplied: make(map[string]application.IconEventIdentity),
iconFailures: make(map[string]application.IconFailureCode),
iconFailures: make(map[string]iconFailureState),
}
shell.search.SingleLine = true
shell.SetItems(items)
@@ -82,7 +82,7 @@ func (shell *AppShell) SetItems(items []application.CatalogListItem) {
nextReferences := make(map[string]string, len(items))
nextRequests := make(map[string]application.IconEventIdentity, len(items))
nextApplied := make(map[string]application.IconEventIdentity, len(items))
nextFailures := make(map[string]application.IconFailureCode, len(items))
nextFailures := make(map[string]iconFailureState, len(items))
for _, item := range items {
controls := shell.rows[item.ID]
if controls == nil {
@@ -101,7 +101,8 @@ func (shell *AppShell) SetItems(items []application.CatalogListItem) {
if applied, exists := shell.iconApplied[item.ID]; exists && applied.Reference == reference {
nextApplied[item.ID] = applied
}
if failure, exists := shell.iconFailures[item.ID]; exists {
if failure, exists := shell.iconFailures[item.ID]; exists &&
failure.Identity.Reference == reference {
nextFailures[item.ID] = failure
}
}
+58
View File
@@ -12,6 +12,8 @@ import (
"softbox.local/core/domain"
)
const unsafeIconCacheMessage = "检测到不安全的图标缓存项。该缓存项未被使用,本次请求没有继续远端获取或自动修复。请完全退出 SoftBox 后,按故障排查文档由管理员人工处理。"
func (shell *AppShell) layoutDetail(
gtx layout.Context,
theme *material.Theme,
@@ -42,6 +44,9 @@ func (shell *AppShell) layoutDetail(
}),
)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return shell.layoutUnsafeIconCacheFailure(gtx, theme, item.ID)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
@@ -120,6 +125,59 @@ func (shell *AppShell) layoutDetail(
)
}
func (shell *AppShell) layoutUnsafeIconCacheFailure(
gtx layout.Context,
theme *material.Theme,
appID string,
) layout.Dimensions {
failure, exists := shell.iconFailures[appID]
if !exists || failure.Code != application.IconFailureUnsafe ||
failure.Identity.AppID != appID {
return layout.Dimensions{}
}
return layout.Inset{Top: unit.Dp(12)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return outlinedPanel(
gtx,
shellColors.destructive,
shellColors.surface,
unit.Dp(8),
layout.UniformInset(unit.Dp(12)),
func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(
gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
title := material.Body1(theme, "图标缓存安全警告")
title.Color = shellColors.destructive
return title.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(material.Body2(theme, unsafeIconCacheMessage).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
diagnostic := material.Caption(theme, unsafeIconCacheDiagnostic(failure))
diagnostic.Color = shellColors.secondary
return diagnostic.Layout(gtx)
}),
)
},
)
})
}
func unsafeIconCacheDiagnostic(failure iconFailureState) string {
return fmt.Sprintf(
"诊断码:%s\n应用 ID:%s\n缓存定位符:%s",
failure.Code,
failure.Identity.AppID,
unsafeIconCacheLocator(failure.Identity),
)
}
func unsafeIconCacheLocator(identity application.IconEventIdentity) string {
digest := strings.TrimPrefix(identity.Reference, "sha256:")
return fmt.Sprintf("%s-%d.icon", digest, identity.DPI)
}
func actionLabel(item application.CatalogListItem) string {
if item.Reason != "" || item.Status == domain.StatusIncompatible {
return "查看不可用原因"