internal/ingestion/mediacache/e2_purge_batch_test.go
Rama main · réplica de lectura del repositorio original.
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mediacache
import (
"context"
"testing"
)
// TestPurgeTaskUsesSingleBatchDelete pins the E2 batch call shape in the media
// cache: every entry key plus the quota (digest/state) keys travel in exactly
// ONE Store.DeleteMany call, and the manifest is deleted separately (one
// per-key Delete) only after that batch succeeds. A loop-based implementation
// would pass a happy-path test, so we assert the batch carries the entries and
// quota but NOT the manifest, and the manifest's own Delete is the sole
// per-key call.
func TestPurgeTaskUsesSingleBatchDelete(t *testing.T) {
backend := newFakeStore()
ctx := taskContext("task-1")
store := New(backend, "task-1")
ref, err := store.Put(ctx, "html", "image/png", []byte("hello"))
if err != nil {
t.Fatal(err)
}
if err := PurgeTask(ctx, backend, "task-1"); err != nil {
t.Fatalf("PurgeTask error = %v, want nil", err)
}
if backend.deleteManyCalls != 1 {
t.Errorf("DeleteMany calls = %d, want 1 (single batch)", backend.deleteManyCalls)
}
// Exactly one per-key Delete, for the manifest only.
if len(backend.deleted) != 1 || backend.deleted[0] != manifestKey("task-1") {
t.Errorf("per-key Delete = %v, want exactly the manifest key", backend.deleted)
}
if len(backend.kv) != 0 || len(backend.sets) != 0 {
t.Errorf("leftover after purge kv=%v sets=%v", backend.kv, backend.sets)
}
// The batch must carry the entry key plus the quota keys, but NOT the
// manifest (deleted separately after the batch succeeds).
digestKey, stateKey := quotaKeys("task-1")
entryKey := store.key(ref.SHA256)
required := []string{entryKey, digestKey, stateKey}
for _, k := range required {
found := false
for _, got := range backend.lastDeleteMany {
if got == k {
found = true
break
}
}
if !found {
t.Errorf("batch did not include required key %q (batch=%v)", k, backend.lastDeleteMany)
}
}
for _, got := range backend.lastDeleteMany {
if got == manifestKey("task-1") {
t.Errorf("batch must not include the manifest key; it is deleted separately")
}
}
}
// TestPurgeTaskBatchHandlesEmptyManifest asserts an empty media manifest is still
// reclaimed (manifest + quota keys) in one batch call, not a zero-iteration loop
// followed by separate deletes.
func TestPurgeTaskBatchHandlesEmptyManifest(t *testing.T) {
backend := newFakeStore()
ctx := taskContext("task-1")
if err := PurgeTask(ctx, backend, "task-1"); err != nil {
t.Fatalf("PurgeTask error = %v, want nil", err)
}
if backend.deleteManyCalls != 1 {
t.Errorf("DeleteMany calls = %d, want 1 for an empty manifest", backend.deleteManyCalls)
}
digestKey, stateKey := quotaKeys("task-1")
if _, ok := backend.kv[digestKey]; ok {
t.Errorf("quota digest key %q not removed", digestKey)
}
if _, ok := backend.kv[stateKey]; ok {
t.Errorf("quota state key %q not removed", stateKey)
}
}
// TestPurgeTaskBatchPropagatesFailure asserts that when an entry batch fails, the
// media manifest is preserved (resumable), not deleted alongside a partially
// reclaimed batch.
func TestPurgeTaskBatchPropagatesFailure(t *testing.T) {
backend := newFakeStore()
ctx := taskContext("task-1")
store := New(backend, "task-1")
ref, err := store.Put(ctx, "html", "image/png", []byte("first"))
if err != nil {
t.Fatal(err)
}
key := store.key(ref.SHA256)
backend.deleteFails = map[string]bool{key: true}
if err := PurgeTask(ctx, backend, "task-1"); err == nil {
t.Fatal("expected error when an entry cannot be deleted")
}
if len(backend.sets[manifestKey("task-1")]) == 0 {
t.Fatal("manifest must survive a failed entry batch (resumable)")
}
}
// TestPurgeTaskManifestSurvivesEntryFailure asserts that when an entry batch
// fails, the media manifest is preserved (resumable), not deleted alongside a
// partially-reclaimed batch. This must hold regardless of how many DEL batches
// the entry list is split into.
func TestPurgeTaskManifestSurvivesEntryFailure(t *testing.T) {
backend := newFakeStore()
ctx := taskContext("task-1")
store := New(backend, "task-1")
ref, err := store.Put(ctx, "html", "image/png", []byte("first"))
if err != nil {
t.Fatal(err)
}
key := store.key(ref.SHA256)
backend.deleteFails = map[string]bool{key: true}
if err := PurgeTask(ctx, backend, "task-1"); err == nil {
t.Fatal("expected error when an entry cannot be deleted")
}
if len(backend.sets[manifestKey("task-1")]) == 0 {
t.Fatal("manifest must survive a failed entry batch (resumable)")
}
}
// TestPurgeTaskBatchNoopWithoutTaskOrStore pins that the batch path issues no
// DeleteMany when there is nothing to purge.
func TestPurgeTaskBatchNoopWithoutTaskOrStore(t *testing.T) {
backend := newFakeStore()
PurgeTask(context.Background(), backend, "")
if backend.deleteManyCalls != 0 {
t.Errorf("DeleteMany calls = %d, want 0 for an empty task id", backend.deleteManyCalls)
}
PurgeTask(context.Background(), nil, "task-1")
}