Files
story/internal/privacy/delete_processor_test.go
T

96 lines
3.7 KiB
Go
Raw Normal View History

2026-08-26 06:41:07 +08:00
package privacy
import (
"context"
"errors"
"strings"
"testing"
"time"
"unicode/utf8"
"data-terminal-cloud/internal/outbox"
2026-08-26 06:41:07 +08:00
)
func TestDeleteProcessorValidationAndSafeErrors(t *testing.T) {
if _, err := NewAccessProcessor(nil); err == nil {
t.Fatal("nil access database was accepted")
}
if _, err := NewDeleteProcessor(nil, nil, nil); err == nil {
t.Fatal("invalid deletion dependencies were accepted")
}
processor := &DeleteProcessor{}
if _, _, err := processor.executeSubtask(context.Background(), deletionRequest{}, "unknown"); err == nil || !strings.Contains(err.Error(), "unsupported") {
t.Fatalf("unsupported subsystem error=%v", err)
}
if got := safeDeletionError(nil); got != "" {
t.Fatalf("nil error text=%q", got)
}
if got := safeDeletionError(errors.New("temporary object-store failure")); got != "temporary object-store failure" {
t.Fatalf("short safe error=%q", got)
}
longText := strings.Repeat("x", 600)
if got := safeDeletionError(errors.New(longText)); len(got) != 450 || strings.Contains(got, "secret") {
t.Fatalf("truncated safe error length=%d", len(got))
}
}
func TestDeletionRetryCooldownMatchesReceipt(t *testing.T) {
cause := errors.New("temporary deletion dependency failure")
for _, test := range []struct {
name string
err error
want time.Duration
}{
{"no supplier delay", outbox.Retryable(cause), 30 * time.Second},
{"short supplier delay", outbox.RetryAfter(cause, time.Second), 30 * time.Second},
{"long supplier delay", outbox.RetryAfter(cause, 2*time.Minute), 2 * time.Minute},
} {
t.Run(test.name, func(t *testing.T) {
got := deletionRetryError(test.err)
if !outbox.IsRetryable(got) || outbox.RetryDelay(got) != test.want || !errors.Is(got, cause) {
t.Fatalf("retry metadata or cause changed: %v delay=%v", got, outbox.RetryDelay(got))
}
})
}
if got := deletionRetryError(cause); got != cause || outbox.IsRetryable(got) {
t.Fatalf("permanent failure became retryable: %v", got)
}
if got := deletionRetryError(nil); got != nil {
t.Fatalf("nil failure became non-nil: %v", got)
}
}
func TestSafeDeletionErrorRetainsValidUTF8(t *testing.T) {
// The former 450-byte slice cut the first Chinese character in this message.
message := strings.Repeat("x", 449) + "删除依赖暂不可用"
got := safeDeletionError(errors.New(message))
if !utf8.ValidString(got) || utf8.RuneCountInString(got) != 450 || !strings.HasSuffix(got, "删") {
t.Fatalf("failure receipt is not bounded valid text: bytes=%d runes=%d", len(got), utf8.RuneCountInString(got))
}
if got := safeDeletionError(errors.New("短错误")); got != "短错误" {
t.Fatalf("short Unicode error changed: %q", got)
}
}
func TestCallMemoryDeletionReportsOutstandingProofs(t *testing.T) {
if err := (callMemoryDeletionPending{}).err(); err != nil {
t.Fatalf("fully reconciled state was blocked: %v", err)
}
for _, test := range []struct {
pending callMemoryDeletionPending
want string
}{
{callMemoryDeletionPending{Projection: true}, "memory_projection"},
{callMemoryDeletionPending{Runtime: true}, "runtime_stop_confirmation"},
{callMemoryDeletionPending{Backup: true}, "backup_cleanup_evidence"},
{callMemoryDeletionPending{LegacyProjection: true}, "legacy_projection"},
{callMemoryDeletionPending{LegacyCleanup: true}, "legacy_cleanup_evidence"},
{callMemoryDeletionPending{Runtime: true, Backup: true}, "runtime_stop_confirmation, backup_cleanup_evidence"},
} {
err := test.pending.err()
if err == nil || err.Error() != "call memory deletion pending: "+test.want || !outbox.IsRetryable(err) || outbox.RetryDelay(err) != time.Minute {
t.Fatalf("missing cleanup evidence was hidden or acknowledged: want=%q error=%v", test.want, err)
}
}
}