mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
132 lines
6.0 KiB
Java
132 lines
6.0 KiB
Java
package vip.mate.channel.web;
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
/**
|
|
* Pins {@link ChatStreamTracker#cleanupStaleRuns()} — the switch from
|
|
* wall-clock {@code MAX_LIFETIME_MS} to inactivity-based eviction.
|
|
*
|
|
* <p>Before the fix, a long-running agent that kept producing tool calls
|
|
* (47-minute LLM-review smoke test, round 6) was killed at the 30-minute
|
|
* wall-clock mark mid-task. The new behaviour mirrors hermes-agent's
|
|
* {@code gateway_timeout}: only completely idle runs are evicted, the
|
|
* actively-producing ones can run as long as they need to.
|
|
*/
|
|
class ChatStreamTrackerCleanupTest {
|
|
|
|
private static ChatStreamTracker newTracker() {
|
|
ChatStreamTracker t = new ChatStreamTracker(new ObjectMapper());
|
|
// Tighten the idle threshold so the test stays at unit-test speed.
|
|
t.setIdleTimeoutMinutesForTesting(5);
|
|
return t;
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Active run (recent lastEventAt) survives cleanup regardless of total age.")
|
|
void activeRunSurvives() {
|
|
ChatStreamTracker tracker = newTracker();
|
|
tracker.register("conv-active");
|
|
// lastEventAt was set to "now" inside the RunState constructor —
|
|
// no backdate, so even a "very old createdAt" would be irrelevant.
|
|
tracker.cleanupStaleRuns();
|
|
assertTrue(tracker.hasRunStateForTesting("conv-active"),
|
|
"actively-producing run must not be evicted");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Idle run beyond threshold is evicted.")
|
|
void idleRunEvicted() {
|
|
ChatStreamTracker tracker = newTracker();
|
|
tracker.register("conv-idle");
|
|
// 6 minutes ago — past the 5-minute threshold set above.
|
|
tracker.backdateLastEventForTesting("conv-idle", System.currentTimeMillis() - 6 * 60_000L);
|
|
tracker.cleanupStaleRuns();
|
|
assertFalse(tracker.hasRunStateForTesting("conv-idle"),
|
|
"idle run past threshold must be evicted");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Idle run just inside the threshold survives — no premature eviction.")
|
|
void idleRunWithinThresholdSurvives() {
|
|
ChatStreamTracker tracker = newTracker();
|
|
tracker.register("conv-borderline");
|
|
// 4 minutes idle — within 5-minute window.
|
|
tracker.backdateLastEventForTesting("conv-borderline", System.currentTimeMillis() - 4 * 60_000L);
|
|
tracker.cleanupStaleRuns();
|
|
assertTrue(tracker.hasRunStateForTesting("conv-borderline"),
|
|
"run idle below threshold must survive — would otherwise be a regression of the wall-clock bug");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Mixed: active + idle runs — only the idle one is evicted.")
|
|
void mixedRunsSelectiveEviction() {
|
|
ChatStreamTracker tracker = newTracker();
|
|
tracker.register("conv-active");
|
|
tracker.register("conv-idle");
|
|
tracker.backdateLastEventForTesting("conv-idle", System.currentTimeMillis() - 10 * 60_000L);
|
|
tracker.cleanupStaleRuns();
|
|
assertTrue(tracker.hasRunStateForTesting("conv-active"));
|
|
assertFalse(tracker.hasRunStateForTesting("conv-idle"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Default idle timeout from @Value matches the documented 30-minute fallback.")
|
|
void defaultIdleTimeoutIs30() {
|
|
// Bypass the @Value injection (no Spring context in this unit test) —
|
|
// the field initialiser pins the default so a refactor that drops the
|
|
// = 30 falls over here.
|
|
ChatStreamTracker tracker = new ChatStreamTracker(new ObjectMapper());
|
|
assertEquals(30, tracker.idleTimeoutMinutesForTesting());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Eviction fires emergencySaveCallback first so the assistant trace survives the dispose.")
|
|
void evictionTriggersEmergencySave() {
|
|
ChatStreamTracker tracker = newTracker();
|
|
tracker.register("conv-needs-save");
|
|
|
|
AtomicInteger saveCount = new AtomicInteger();
|
|
tracker.setEmergencySaveCallback("conv-needs-save", saveCount::incrementAndGet);
|
|
|
|
// Backdate so the eviction path triggers.
|
|
tracker.backdateLastEventForTesting("conv-needs-save", System.currentTimeMillis() - 10 * 60_000L);
|
|
tracker.cleanupStaleRuns();
|
|
|
|
assertEquals(1, saveCount.get(),
|
|
"emergency save must run exactly once before eviction disposes the Flux");
|
|
assertFalse(tracker.hasRunStateForTesting("conv-needs-save"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("Completed (done) runs skip the emergency save — they already saved at doOnComplete.")
|
|
void doneRunsSkipEmergencySaveOnEviction() {
|
|
ChatStreamTracker tracker = newTracker();
|
|
tracker.register("conv-already-done");
|
|
tracker.complete("conv-already-done"); // mark done
|
|
// Drive its retention timer out by backdating createdAt; cleanup
|
|
// path for done runs uses age, not lastEventAt.
|
|
tracker.backdateLastEventForTesting("conv-already-done", System.currentTimeMillis() - 10 * 60_000L);
|
|
|
|
AtomicInteger saveCount = new AtomicInteger();
|
|
tracker.setEmergencySaveCallback("conv-already-done", saveCount::incrementAndGet);
|
|
// Tweak retention so the done branch fires.
|
|
// (DONE_RETENTION_MS is 5 min; backdate lastEventAt above already
|
|
// exceeds it relative to createdAt — but createdAt isn't backdated,
|
|
// so the done branch won't trigger here. The point is: even if it
|
|
// did, the emergency save should be skipped because done=true.)
|
|
// Run cleanup — done run with recent createdAt won't be evicted at
|
|
// all, so the callback shouldn't fire.
|
|
tracker.cleanupStaleRuns();
|
|
assertEquals(0, saveCount.get(),
|
|
"callback must not fire when the run is marked done — that path already saved");
|
|
}
|
|
}
|