082ab08e0b
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
83 lines
3.4 KiB
TypeScript
83 lines
3.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { createCompletionTracker } from "../src/lib/completion-notify";
|
|
import type { ObservedSession } from "../src/lib/completion-notify";
|
|
import type { SessionStatus } from "@prismshadow/penguin-server/api";
|
|
|
|
const row = (sessionId: string, status: SessionStatus): ObservedSession => ({
|
|
sessionId,
|
|
status,
|
|
});
|
|
|
|
describe("createCompletionTracker", () => {
|
|
it("fires when a known running session goes idle", () => {
|
|
const t = createCompletionTracker();
|
|
expect(t.observe([row("a", "running")], 0)).toEqual([]);
|
|
expect(t.observe([row("a", "idle")], 1000)).toEqual(["a"]);
|
|
});
|
|
|
|
it("treats compacting as still active and fires only on the flip to idle", () => {
|
|
const t = createCompletionTracker();
|
|
t.observe([row("a", "running")], 0);
|
|
expect(t.observe([row("a", "compacting")], 100)).toEqual([]);
|
|
expect(t.observe([row("a", "idle")], 200)).toEqual(["a"]);
|
|
});
|
|
|
|
it("never fires on first observation, even an idle one after a running list load", () => {
|
|
const t = createCompletionTracker();
|
|
// A list that loads with idle sessions has witnessed nothing.
|
|
expect(t.observe([row("a", "idle"), row("b", "idle")], 0)).toEqual([]);
|
|
expect(t.observe([row("a", "idle"), row("b", "idle")], 100)).toEqual([]);
|
|
});
|
|
|
|
it("dedupes per run: repeated idle observations fire once", () => {
|
|
const t = createCompletionTracker();
|
|
t.observe([row("a", "running")], 0);
|
|
expect(t.observe([row("a", "idle")], 1000)).toEqual(["a"]);
|
|
expect(t.observe([row("a", "idle")], 2000)).toEqual([]);
|
|
expect(t.observe([row("a", "idle")], 60_000)).toEqual([]);
|
|
});
|
|
|
|
it("fires again for a genuinely new run after the cooldown", () => {
|
|
const t = createCompletionTracker(10_000);
|
|
t.observe([row("a", "running")], 0);
|
|
expect(t.observe([row("a", "idle")], 1000)).toEqual(["a"]);
|
|
t.observe([row("a", "running")], 20_000);
|
|
expect(t.observe([row("a", "idle")], 30_000)).toEqual(["a"]);
|
|
});
|
|
|
|
it("suppresses a stale-snapshot flap within the cooldown window", () => {
|
|
const t = createCompletionTracker(10_000);
|
|
t.observe([row("a", "running")], 0);
|
|
expect(t.observe([row("a", "idle")], 1000)).toEqual(["a"]);
|
|
// A stale reload snapshot resurrects "running", then the fresh state lands again:
|
|
// the same finish must not notify twice.
|
|
t.observe([row("a", "running")], 1100);
|
|
expect(t.observe([row("a", "idle")], 1200)).toEqual([]);
|
|
});
|
|
|
|
it("tracks sessions independently and reports multiple completions", () => {
|
|
const t = createCompletionTracker();
|
|
t.observe([row("a", "running"), row("b", "running"), row("c", "idle")], 0);
|
|
expect(t.observe([row("a", "idle"), row("b", "idle"), row("c", "idle")], 1000)).toEqual([
|
|
"a",
|
|
"b",
|
|
]);
|
|
});
|
|
|
|
it("prunes sessions that leave the list: reappearing is a fresh first observation", () => {
|
|
const t = createCompletionTracker();
|
|
t.observe([row("a", "running")], 0);
|
|
// Project switch empties the list; the session's state is dropped.
|
|
t.observe([], 100);
|
|
expect(t.observe([row("a", "idle")], 200)).toEqual([]);
|
|
});
|
|
|
|
it("ignores duplicate ids within one observation (first occurrence wins)", () => {
|
|
const t = createCompletionTracker();
|
|
t.observe([row("a", "running")], 0);
|
|
expect(t.observe([row("a", "idle"), row("a", "running")], 1000)).toEqual(["a"]);
|
|
// The duplicate "running" row did not restart the run.
|
|
expect(t.observe([row("a", "idle")], 60_000)).toEqual([]);
|
|
});
|
|
});
|