package privacy import ( "context" "errors" "strings" "testing" "time" "unicode/utf8" "data-terminal-cloud/internal/outbox" ) 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) } } }