Refuse cacheAs files written before the scores were corrected

A cacheAs file holds a finished analysis, association scores included,
and 1.4.0 computes those differently. A file written by 1.3.0 would
therefore be handed back with numbers that would not be arrived at again,
and the comparison of parameters added during this cycle cannot notice
it: the parameters did not change, the formula did. The version that
wrote a file is now recorded in it, and one from before 1.4.0 is
recomputed and overwritten, with a warning saying why.

The machinery moves to a file of its own on the way, since it is no
longer about collocation analysis alone, and verbose drops out of what a
file records: how loud a query is does not change what it returns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: I8af9eac21ae53fe5d583578c3689b37d140a4af4
diff --git a/tests/testthat/test-collocation-cache-parameters.R b/tests/testthat/test-collocation-cache-parameters.R
index 06e8ca1..a207a8f 100644
--- a/tests/testthat/test-collocation-cache-parameters.R
+++ b/tests/testthat/test-collocation-cache-parameters.R
@@ -26,8 +26,10 @@
 
   collocationAnalysis(offlineConnection(), "Test", minOccur = 3, cacheAs = cacheFile)
 
-  stored <- attr(readRDS(cacheFile), RKorAPClient:::collocationCacheAttribute)
+  stored <- attr(readRDS(cacheFile), RKorAPClient:::cacheAsAttribute)
   expect_false(is.null(stored))
+  expect_equal(stored$scoreVersion, RKorAPClient:::cacheAsScoreVersion)
+  expect_equal(stored$packageVersion, as.character(utils::packageVersion("RKorAPClient")))
   expect_equal(stored$parameters$node, "Test")
   expect_equal(stored$parameters$minOccur, 3)
   expect_equal(stored$apiUrl, "https://example.invalid/")
@@ -52,7 +54,7 @@
 
   expect_equal(fromCache, fresh)
   # the parameters live in the file only, not in the returned value
-  expect_null(attr(fromCache, RKorAPClient:::collocationCacheAttribute))
+  expect_null(attr(fromCache, RKorAPClient:::cacheAsAttribute))
 })
 
 test_that("an unchanged call is served from the cache without contacting the server", {
@@ -124,51 +126,83 @@
     )
   })
 
-  stored <- attr(readRDS(cacheFile), RKorAPClient:::collocationCacheAttribute)
+  stored <- attr(readRDS(cacheFile), RKorAPClient:::cacheAsAttribute)
   expect_equal(stored$parameters$minOccur, 5)
 })
 
-test_that("cache files written without parameters are still used", {
+test_that("cache files written before the scores were corrected are refused", {
+  mockEmptyAnalysis()
   cacheFile <- tempfile(fileext = ".rds")
   on.exit(unlink(cacheFile), add = TRUE)
   kco <- offlineConnection()
 
-  # as written by RKorAPClient 1.3.0
+  # as written by RKorAPClient 1.3.0, whose logDice and ll differ from today's
   legacy <- tibble::tibble(node = "Test", collocate = "c", logDice = 7)
   saveRDS(legacy, cacheFile)
 
-  testthat::local_mocked_bindings(
-    collocatesQuery = function(...) stop("server must not be contacted"),
-    .package = "RKorAPClient"
+  expect_warning(
+    result <- collocationAnalysis(kco, "Test", cacheAs = cacheFile),
+    "logDice"
   )
-  expect_equal(collocationAnalysis(kco, "Test", cacheAs = cacheFile), legacy)
+  expect_false(identical(result, legacy))
+  # and the stale file is replaced by one that records what wrote it
+  expect_false(is.null(attr(readRDS(cacheFile), RKorAPClient:::cacheAsAttribute)))
 })
 
-test_that("differingCollocationCacheParameters reports what changed", {
-  differing <- RKorAPClient:::differingCollocationCacheParameters
+test_that("cache files of an older version are refused, naming it", {
+  mockEmptyAnalysis()
+  cacheFile <- tempfile(fileext = ".rds")
+  on.exit(unlink(cacheFile), add = TRUE)
+  kco <- offlineConnection()
+
+  collocationAnalysis(kco, "Test", cacheAs = cacheFile)
+  aged <- readRDS(cacheFile)
+  record <- attr(aged, RKorAPClient:::cacheAsAttribute)
+  record$scoreVersion <- "1.3.0"
+  record$packageVersion <- "1.3.0"
+  attr(aged, RKorAPClient:::cacheAsAttribute) <- record
+  saveRDS(aged, cacheFile)
+
+  expect_warning(collocationAnalysis(kco, "Test", cacheAs = cacheFile), "1\\.3\\.0")
+})
+
+test_that("cacheAsRejectionReason says why a cache file cannot be used", {
+  reason <- RKorAPClient:::cacheAsRejectionReason
 
   stored <- list(
+    scoreVersion = RKorAPClient:::cacheAsScoreVersion,
     parameters = list(node = "Test", minOccur = 3, vc = ""),
     dots = list(),
     apiUrl = "https://korap.ids-mannheim.de/api/v1.0/"
   )
 
-  expect_equal(differing(stored, stored), character(0))
+  expect_null(reason(stored, stored))
+  expect_match(reason(NULL, stored), "before RKorAPClient")
+
+  # a file from before the corrections: an older score generation, and the
+  # version that wrote it, which the reason names
+  aged <- stored
+  aged$scoreVersion <- "1.3.0"
+  aged$packageVersion <- "1.3.0"
+  expect_match(reason(aged, stored), "written by RKorAPClient 1\\.3\\.0")
+
+  # one that does not even say what wrote it
+  anonymous <- stored
+  anonymous$scoreVersion <- NULL
+  expect_match(reason(anonymous, stored), "written before RKorAPClient")
 
   changed <- stored
   changed$parameters$minOccur <- 5
-  expect_equal(differing(stored, changed), "minOccur")
+  expect_match(reason(stored, changed), "minOccur")
 
-  changed <- stored
-  changed$parameters$minOccur <- 5
   changed$parameters$vc <- "textType=/Zeit.*/"
-  expect_setequal(differing(stored, changed), c("minOccur", "vc"))
+  expect_match(reason(stored, changed), "minOccur, vc")
 
   changed <- stored
   changed$dots <- list(smoothingConstant = 1)
-  expect_equal(differing(stored, changed), "...")
+  expect_match(reason(stored, changed), "\\.\\.\\.")
 
   changed <- stored
   changed$apiUrl <- "https://korap.dnb.de/api/v1.0/"
-  expect_equal(differing(stored, changed), "KorAP instance")
+  expect_match(reason(stored, changed), "KorAP instance")
 })