From 5ea6c215a48bbcd09c7c82874b4843b2307ab214 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 14:36:24 +0200 Subject: [PATCH] issue 1840/S1: let a process delete a key, as a tools bundle can --- go/process/state.go | 14 ++++++++++++++ go/process/state_integration_test.go | 28 ++++++++++++++++++++++++++++ go/process/state_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/go/process/state.go b/go/process/state.go index 1e79302..07d62b4 100644 --- a/go/process/state.go +++ b/go/process/state.go @@ -263,3 +263,17 @@ func (s State) Watch(key string, handle func(stdio.StateChange) error) error { }() return nil } + +// Delete removes a key. A key that was not there is not an error. +func (s State) Delete(key string) error { + ctx, cancel := context.WithTimeout(context.Background(), stateTimeout) + defer cancel() + kv, err := s.store(ctx) + if err != nil { + return err + } + if err := kv.Delete(ctx, key); err != nil && !errors.Is(err, jetstream.ErrKeyNotFound) { + return fmt.Errorf("%s's state %s, key %s: %w", s.Module, s.Name, key, err) + } + return nil +} diff --git a/go/process/state_integration_test.go b/go/process/state_integration_test.go index f0b1cfd..eca097a 100644 --- a/go/process/state_integration_test.go +++ b/go/process/state_integration_test.go @@ -234,3 +234,31 @@ func TestStateWatchRefusesAnUngrantedBucketNamingIt(t *testing.T) { t.Fatalf("Watch on an ungranted bucket: %v, want a refusal naming mesh-mail_unrelated", err) } } + +// 1840/S1: given a process connection granted its bucket, when it deletes a key, then the key is gone and +// deleting it again is no error. +func TestStateDeleteOnAGrantedBucket(t *testing.T) { + url, account := grantedSendProcess(t) + _, adminJetStream := adminJS(t, url) + if _, err := adminJetStream.CreateKeyValue(ctxFor(t), jetstream.KeyValueConfig{Bucket: "mesh-mail_sends"}); err != nil { + t.Fatalf("the admin could not make the bucket: %v", err) + } + conn, err := Connect(account) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + s := conn.State("sends") + if _, err := s.Put("k", 1); err != nil { + t.Fatal(err) + } + if err := s.Delete("k"); err != nil { + t.Fatalf("Delete on a granted bucket: %v", err) + } + if e, err := s.Get("k"); err != nil || e != nil { + t.Fatalf("Get after Delete: %+v, %v", e, err) + } + if err := s.Delete("k"); err != nil { + t.Fatalf("Delete of an absent key: %v", err) + } +} diff --git a/go/process/state_test.go b/go/process/state_test.go index 847abd2..02b084a 100644 --- a/go/process/state_test.go +++ b/go/process/state_test.go @@ -82,6 +82,14 @@ func (f *fakeKV) Put(_ context.Context, key string, value []byte) (uint64, error return f.next, nil } +func (f *fakeKV) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error { + f.mu.Lock() + defer f.mu.Unlock() + delete(f.values, key) + delete(f.revs, key) + return nil +} + func (f *fakeKV) Keys(_ context.Context, _ ...jetstream.WatchOpt) ([]string, error) { f.mu.Lock() defer f.mu.Unlock() @@ -277,3 +285,20 @@ type silentKV struct{ jetstream.KeyValue } func (silentKV) Watch(context.Context, string, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) { return &fakeWatcher{updates: make(chan jetstream.KeyValueEntry)}, nil } + +// 1840/S1: given a key, when it is deleted, then it reads as absent; deleting a key that was not there is no error. +func TestStateDeleteRemovesAKeyAndAnAbsentOneIsNoError(t *testing.T) { + s := stateOn(newFakeKV()) + if _, err := s.Put("k", 1); err != nil { + t.Fatal(err) + } + if err := s.Delete("k"); err != nil { + t.Fatalf("Delete of a key: %v", err) + } + if e, err := s.Get("k"); err != nil || e != nil { + t.Fatalf("Get after Delete: %+v, %v, want absent", e, err) + } + if err := s.Delete("never"); err != nil { + t.Fatalf("Delete of an absent key: %v", err) + } +}