From 701ed9b94b9cfccbf7b7cf2d94699c1c06dc420c Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Mon, 26 Oct 2026 23:54:24 +0000 Subject: [PATCH] Test that reconciliation drops a variant's row only when its file is missing reconcileVariantRows is given two tracked variants. The first one's file is a symbolic link to itself, so reading its size fails with an error other than a missing file. The test wants its row kept, with a warning naming its key and the error. The second one has no file, and the test wants its row dropped with the info line for a missing file. The test fails on next, which drops both rows and logs both files as missing. The fix follows in the next commit. Model: opus-5-5 --- internal/imgcache/eviction_internal_test.go | 86 +++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/internal/imgcache/eviction_internal_test.go b/internal/imgcache/eviction_internal_test.go index 15b2ebc..ed7b4eb 100644 --- a/internal/imgcache/eviction_internal_test.go +++ b/internal/imgcache/eviction_internal_test.go @@ -699,6 +699,92 @@ func TestStartEvictionReconcilesAccountingWithDisk(t *testing.T) { } } +// logLineNaming returns the first line of logs, written by slog's JSON +// handler, that is at level and names the variant key, or "" if none is. +func logLineNaming(logs, level string, key imagerequest.VariantKey) string { + for line := range strings.Lines(logs) { + if strings.Contains(line, `"level":"`+level+`"`) && + strings.Contains(line, `"cache_key":"`+string(key)+`"`) { + return line + } + } + + return "" +} + +// TestReconcileVariantRowsDropsOnlyRowsOfMissingFiles gives +// reconcileVariantRows two tracked variants. The first one's file is a +// symbolic link to itself, so its size cannot be read. Its row must stay, +// with a warning naming its key and the error. The second one has no file. +// Its row must go, with the info line for a missing file. +func TestReconcileVariantRowsDropsOnlyRowsOfMissingFiles(t *testing.T) { + t.Parallel() + + cache, _ := newEvictionTestCache(t, 1<<30) + storeEvictionTestVariant(t, cache, testVariantKeyOne, []byte("variant one")) + storeEvictionTestVariant(t, cache, testVariantKeyTwo, []byte("variant two")) + + unreadablePath := cache.variants.keyToPath(string(testVariantKeyOne)) + + err := os.Remove(unreadablePath) + if err != nil { + t.Fatalf("failed to remove the first variant's file: %v", err) + } + + err = os.Symlink(unreadablePath, unreadablePath) + if err != nil { + t.Fatalf("failed to link the first variant's path to itself: %v", err) + } + + _, statErr := os.Stat(unreadablePath) + if statErr == nil || errors.Is(statErr, fs.ErrNotExist) { + t.Fatalf("os.Stat() of the first variant: error = %v, "+ + "want an error other than a missing file", statErr) + } + + err = os.Remove(cache.variants.keyToPath(string(testVariantKeyTwo))) + if err != nil { + t.Fatalf("failed to remove the second variant's file: %v", err) + } + + var logBuf bytes.Buffer + + cache.log = slog.New(slog.NewJSONHandler(&logBuf, nil)) + + err = cache.reconcileVariantRows(t.Context()) + t.Logf("reconcileVariantRows() error = %v, log: %s", err, logBuf.String()) + + if err != nil { + t.Fatalf("reconcileVariantRows() error = %v", err) + } + + rowQuery := `SELECT COUNT(*) FROM variant_content WHERE cache_key = ?` + + if n := countRows(t, cache, rowQuery, string(testVariantKeyOne)); n != 1 { + t.Errorf("the variant whose size cannot be read has %d rows, want 1", n) + } + + if n := countRows(t, cache, rowQuery, string(testVariantKeyTwo)); n != 0 { + t.Errorf("the variant with no file has %d rows, want 0", n) + } + + logs := logBuf.String() + + warning := logLineNaming(logs, "WARN", testVariantKeyOne) + if !strings.Contains(warning, statErr.Error()) { + t.Errorf("no warning names %s and the error %q", testVariantKeyOne, statErr) + } + + if line := logLineNaming(logs, "INFO", testVariantKeyOne); line != "" { + t.Errorf("the variant whose size cannot be read was logged at info: %s", line) + } + + dropped := logLineNaming(logs, "INFO", testVariantKeyTwo) + if !strings.Contains(dropped, "dropped accounting row for missing variant file") { + t.Errorf("no info line says the file of %s is missing", testVariantKeyTwo) + } +} + // TestPeriodicReconciliationAdoptsFileThatAppearsAfterStartup proves // reconciliation is not a one-shot startup-only pass: it must also run // on the periodic ticker, so a variant file that lands on disk with no