2026-08-26 06:41:07 +08:00
|
|
|
package privacy
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
2026-10-08 10:06:44 +08:00
|
|
|
"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))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-08 10:06:44 +08:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|