package agent import ( "bytes" "context" "errors" "log/slog" "os" "path/filepath" "strings" "testing" "time" "git.ipao.vip/rogee/go-sip/internal/logging" ) func captureRecoveryLogs(t *testing.T) *bytes.Buffer { t.Helper() prior := slog.Default() out := new(bytes.Buffer) logging.Configure("agent", false, out) t.Cleanup(func() { slog.SetDefault(prior) }) return out } func TestRecoveryLogsEachOrphanCauseAndLeavesOriginalFiles(t *testing.T) { out := captureRecoveryLogs(t) root := privateRecoveryRoot(t) recovery := RecordingRecovery{Root: root, writeState: func(string, any) error { return errors.New("metadata disk full") }} entry := failedRecordingFixture() if _, err := recovery.SaveFailure([]byte("RIFF private-audio"), entry, &UploadHTTPError{StatusCode: 503}); err == nil { t.Fatal("fault hidden") } out.Reset() recovery.writeState = nil _, err := recovery.RecoverDue(context.Background(), nil, nil) if !errors.Is(err, ErrRecordingRecoveryIncomplete) { t.Fatal(err) } for _, part := range []string{"recovery_validate", "recovery_digest", "original audio has no call information"} { if !strings.Contains(out.String(), part) { t.Fatalf("missing %s: %s", part, out.String()) } } if strings.Contains(out.String(), "private-audio") { t.Fatal("audio leaked") } if _, err := os.Stat(filepath.Join(root, entry.Bucket, filepath.FromSlash(entry.ObjectKey), "recording.wav")); err != nil { t.Fatal(err) } } func TestRecoveryLogsGrantCauseIdentityAndPersistedBackoff(t *testing.T) { out := captureRecoveryLogs(t) now := time.Date(2026, 10, 1, 10, 0, 0, 0, time.UTC) recovery := RecordingRecovery{Root: privateRecoveryRoot(t), Now: func() time.Time { return now }} entry, err := recovery.SaveFailure([]byte("RIFF private-audio"), failedRecordingFixture(), &UploadHTTPError{StatusCode: 503}) if err != nil { t.Fatal(err) } now = now.Add(time.Minute) out.Reset() expected := errors.New("temporary Dispatcher outage") _, err = recovery.RecoverDue(context.Background(), func(context.Context, RecordingRecoveryEntry) (RecoveryTarget, error) { return RecoveryTarget{}, expected }, func(context.Context, RecordingRecoveryEntry) error { return nil }) if !errors.Is(err, expected) { t.Fatalf("original grant failure was lost: %v", err) } for _, part := range []string{entry.SourceEventID, entry.RecordingID, entry.UploadID, "request_upload_grant", "temporary Dispatcher outage", "next_attempt_at", `"attempts":2`} { if !strings.Contains(out.String(), part) { t.Fatalf("missing %s: %s", part, out.String()) } } pending, err := recovery.Load(entry.Bucket, entry.ObjectKey) if err != nil || pending.Attempts != 2 || pending.State != "retry_pending" { t.Fatal(pending, err) } out.Reset() now = now.Add(time.Minute) if _, err = recovery.RecoverDue(context.Background(), nil, nil); err != nil { t.Fatal(err) } if !strings.Contains(out.String(), "retry_wait") || !strings.Contains(out.String(), entry.SourceEventID) { t.Fatal(out.String()) } } func TestResultJournalLogsOriginalDecodeCause(t *testing.T) { out := captureRecoveryLogs(t) j := ResultJournal{Root: privateRecoveryRoot(t)} entry := ResultEntry{SourceEventID: "corrupt-result-event", DispatcherID: "11111111-1111-4111-8111-111111111111", TenantID: 1, Payload: []byte(`{"recording":{}}`)} if err := j.SaveReady(entry); err != nil { t.Fatal(err) } path, err := j.path(entry.SourceEventID) if err != nil { t.Fatal(err) } if err = os.WriteFile(path, []byte("{"), 0o600); err != nil { t.Fatal(err) } err = j.Recover(context.Background(), func(context.Context, ResultEntry) error { return nil }) if err == nil || !strings.Contains(err.Error(), "unexpected end of JSON input") { t.Fatalf("original decode reason was hidden: %v", err) } for _, part := range []string{"decode_original_result", "unexpected end of JSON input", "event_digest"} { if !strings.Contains(out.String(), part) { t.Fatalf("missing %s: %s", part, out.String()) } } if _, err = os.Stat(path); err != nil { t.Fatal("corrupt original file must remain", err) } } func TestResultJournalLogsIndividualCauseAndOriginalIdentity(t *testing.T) { out := captureRecoveryLogs(t) j := ResultJournal{Root: privateRecoveryRoot(t)} entry := ResultEntry{SourceEventID: "result-log-event", DispatcherID: "11111111-1111-4111-8111-111111111111", TenantID: 1, Payload: []byte(`{"recording":{}}`)} if err := j.SaveReady(entry); err != nil { t.Fatal(err) } expected := errors.New("result persistence unavailable") if err := j.Recover(context.Background(), func(context.Context, ResultEntry) error { return expected }); !errors.Is(err, expected) { t.Fatal(err) } for _, part := range []string{"result-log-event", "report_original_result", "result persistence unavailable", "tenant_id"} { if !strings.Contains(out.String(), part) { t.Fatalf("missing %s: %s", part, out.String()) } } }