From 7e920102f67cfbac223a43b3611a897230de94fd Mon Sep 17 00:00:00 2001 From: Mathias Date: Sat, 3 Oct 2026 20:59:40 +0200 Subject: [PATCH] test: SIGTERM cancels the process context; a shutdown mid-job records the row interrupted at once (#50), red Co-Authored-By: Claude Sonnet 5.5 --- cmd/tapir/shutdown_test.go | 30 ++++++++++++++++++++++++++++++ internal/uploads/worker_test.go | 13 +++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 cmd/tapir/shutdown_test.go diff --git a/cmd/tapir/shutdown_test.go b/cmd/tapir/shutdown_test.go new file mode 100644 index 0000000..be808d3 --- /dev/null +++ b/cmd/tapir/shutdown_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "os" + "syscall" + "testing" + "time" +) + +// Kubernetes stops a pod with SIGTERM; only SIGINT was handled, so a deploy killed the +// process without running any shutdown code (#50). +func TestShutdownContext_IsCancelledBySIGTERMAndSIGINT(t *testing.T) { + for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} { + t.Run(sig.String(), func(t *testing.T) { + ctx, stop := shutdownContext(context.Background()) + defer stop() + + if err := syscall.Kill(os.Getpid(), sig); err != nil { + t.Fatalf("send %v: %v", sig, err) + } + + select { + case <-ctx.Done(): + case <-time.After(2 * time.Second): + t.Fatalf("context not cancelled by %v", sig) + } + }) + } +} diff --git a/internal/uploads/worker_test.go b/internal/uploads/worker_test.go index 04b9abe..8fac296 100644 --- a/internal/uploads/worker_test.go +++ b/internal/uploads/worker_test.go @@ -53,7 +53,10 @@ func (s *wStore) CompleteUpload(_ context.Context, _, id string, t domain.Transc return nil } -func (s *wStore) FailUpload(_ context.Context, _, id string, k domain.UploadErrorKind) error { +func (s *wStore) FailUpload(ctx context.Context, _, id string, k domain.UploadErrorKind) error { + if err := ctx.Err(); err != nil { // like the real store: a cancelled context cannot write + return err + } s.mu.Lock() defer s.mu.Unlock() s.failed[id] = k @@ -242,7 +245,9 @@ func TestWorkerProcess_FailuresAreTypedAndStillDeleteTheAudio(t *testing.T) { }) } -func TestWorkerProcess_ShutdownMidJobLeavesTheRowForTheStartupSweep(t *testing.T) { +func TestWorkerProcess_ShutdownMidJobRecordsTheRowInterruptedAtOnce(t *testing.T) { + // SIGTERM on a deploy used to leave the row "transcribing" until the NEXT pod's + // start-up sweep; the process may exit before any sweep runs (#50). r := newWRig(t, time.Minute) r.tr.block = make(chan struct{}) ctx, cancel := context.WithCancel(context.Background()) @@ -255,8 +260,8 @@ func TestWorkerProcess_ShutdownMidJobLeavesTheRowForTheStartupSweep(t *testing.T <-done _, f := r.st.snapshot() - assert.Empty(t, f, "a shutdown is not a transcription failure: the row stays for FailInterruptedUploads to mark interrupted") - assert.Empty(t, r.leftovers(t), "but the audio is still deleted") + assert.Equal(t, domain.UploadErrInterrupted, f["a"], "the row is recorded interrupted by the dying process, not left for a sweep") + assert.Empty(t, r.leftovers(t), "and the audio is still deleted") } func TestWorkerProcess_AJobThatOutlivesItsTimeoutIsFailedNotStuck(t *testing.T) {