Refactor auto-trigger and recovery logic

This commit introduces a significant refactor to the auto-trigger and
recovery mechanisms for the analysis pipeline. The core change is the
introduction of a more granular `CaseAnalysisState` enum, replacing the
simpler `AutoDecision`.

The `evaluate_state` function now computes the full lifecycle state of a
case, considering factors like the presence of recordings, pending
transcripts, queued jobs, failure markers, document staleness, and an
idle threshold. This provides a richer understanding of the case's
status.

The `evaluate_case` function is retained as a backwards-compatible
adapter for existing callsites and tests. It maps the new
`CaseAnalysisState` to the old `AutoDecision` enum, simplifying the
decision to "enqueue" or "skip."

The `recovery` module is also refactored. The `scan_and_enqueue`
function is replaced by `boot_scan_state`, which is now purely
observational. It aggregates statistics about cases in different states
at boot time and logs them, but it no longer attempts to re-enqueue
jobs. The responsibility for triggering analysis now lies with the
per-render `try_enqueue_all_for_user` logic, which utilizes the new
typed state machine.

This refactoring aims to provide more clarity and control over the
analysis pipeline's state management, particularly in handling cases
that have previously failed or are waiting for further input.
This commit is contained in:
2026-05-05 14:26:05 +02:00
parent 9e8db7518d
commit 0660727faf
6 changed files with 403 additions and 252 deletions
+24 -23
View File
@@ -613,35 +613,28 @@ async fn replace_real_case_dry() {
// Recovery
// ---------------------------------------------------------------------
/// Boot-scan is observation-only after the typed-state refactor: even a
/// case sitting in the `Queued` state on disk must NOT be re-enqueued
/// at startup. The per-page-render `try_enqueue_all_for_user` is the
/// only auto-trigger.
#[tokio::test]
async fn recovery_enqueues_pending_analysis() {
async fn recovery_does_not_enqueue_at_boot() {
let tmp = unique_tmpdir("analyze-r1");
let case_dir = tmp.join("dr_a/11111111-1111-1111-1111-111111111111");
std::fs::create_dir_all(&case_dir).unwrap();
std::fs::write(case_dir.join(ANALYSIS_INPUT_FILE), "{}").unwrap();
let (tx, mut rx) = analyze::channel();
analyze::recovery::scan_and_enqueue(&tmp, &tx).await;
let job = rx.try_recv().expect("expected one job");
assert_eq!(job.case_dir, case_dir);
assert!(rx.try_recv().is_err(), "no further jobs expected");
}
#[tokio::test]
async fn recovery_skips_completed_analysis() {
let tmp = unique_tmpdir("analyze-r2");
let case_dir = tmp.join("dr_a/11111111-1111-1111-1111-111111111111");
std::fs::create_dir_all(&case_dir).unwrap();
std::fs::write(case_dir.join(ANALYSIS_INPUT_FILE), "{}").unwrap();
write_document_for_test(&case_dir, "done", "1970-01-01T00:00:00Z");
let (tx, mut rx) = analyze::channel();
analyze::recovery::scan_and_enqueue(&tmp, &tx).await;
let (_tx, mut rx) = analyze::channel();
analyze::recovery::boot_scan_state(
&tmp,
std::time::Duration::ZERO,
std::time::SystemTime::now(),
)
.await;
assert!(
rx.try_recv().is_err(),
"completed analysis must not re-enqueue"
"boot scan must not enqueue any jobs in the new state model"
);
}
@@ -1246,10 +1239,18 @@ async fn recovery_skips_closed_cases() {
)
.unwrap();
let (tx, mut rx) = analyze::channel();
analyze::recovery::scan_and_enqueue(&tmp, &tx).await;
let summary = analyze::recovery::boot_scan_state(
&tmp,
std::time::Duration::ZERO,
std::time::SystemTime::now(),
)
.await;
assert!(rx.try_recv().is_err(), "closed case must not be enqueued");
assert_eq!(
summary,
analyze::recovery::BootStateSummary::default(),
"closed case must not appear in any state bucket"
);
}
// ---------------------------------------------------------------------