Something went wrong. Try again.
last activity notifications for opencode
Something went wrong. Try again.
5.9 kB · 119 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120import { describe, expect, it } from "vitest";import { DEFAULT_CONFIG, type Config } from "../src/config/config.ts";import { renderLine, renderShort, type ActivitySnapshot } from "../src/render/render.ts";
const config = (over: Partial<Config> = {}): Config => ({ ...DEFAULT_CONFIG, ...over });const compact = (over: Partial<Config> = {}): Config => config({ formatStyle: "compact", ...over });
const NOW = 1_000_000_000;const MIN = 60_000;/** prompt 27m ago, part (last LLM evidence) 20m ago — the canonical stalled turn. */const STALLED: ActivitySnapshot = { nowMs: NOW, promptAtMs: NOW - 27 * MIN, partAtMs: NOW - 20 * MIN };
describe("single-clock fallback (the settled clock)", () => { it("renders the bare duration with an ago suffix", () => { expect(renderShort({ nowMs: NOW, settledAtMs: NOW - (5 * MIN + 24_000) }, config())).toBe("5.4m ago"); expect( renderShort({ nowMs: NOW, settledAtMs: NOW - 2 * 3_600_000 }, config({ formatStyle: "compact" })), ).toBe("2h ago"); });
it("reads 'now' while the activity is fresh", () => { expect(renderShort({ nowMs: NOW, settledAtMs: NOW - 1_500 }, config())).toBe("now"); expect(renderShort({ nowMs: NOW, settledAtMs: NOW }, config())).toBe("now"); });
it("holds 'now' while working — but only while the part clock is unknown", () => { const snap: ActivitySnapshot = { nowMs: NOW, settledAtMs: NOW - 10 * MIN, working: true }; expect(renderShort(snap, config())).toBe("now"); expect(renderShort(snap, config({ formatStyle: "verbose" }))).toBe("now"); });
it("marks non-success outcomes", () => { const at = NOW - 5 * MIN; expect(renderShort({ nowMs: NOW, settledAtMs: at, outcome: "failed" }, config())).toBe("5.0m ago ✗"); expect(renderShort({ nowMs: NOW, settledAtMs: at, outcome: "interrupted" }, config())).toBe("5.0m ago ⋯"); const wall = new Date(2026, 7, 20, 14, 32).getTime(); const later = new Date(2026, 7, 20, 20, 0).getTime(); expect( renderShort({ nowMs: later, settledAtMs: wall, outcome: "failed" }, config({ formatStyle: "clock" })), ).toBe("14:32 ✗"); });
it("falls back to the prompt clock when nothing else is known", () => { expect(renderShort({ nowMs: NOW, promptAtMs: NOW - 27 * MIN }, compact())).toBe("27m ago"); expect(renderShort({ nowMs: NOW, promptAtMs: NOW - 27 * MIN, working: true }, compact())).toBe("now"); });
it("renders nothing when no clock is known", () => { expect(renderLine({ nowMs: NOW }, config())).toBe(""); expect(renderShort({ nowMs: NOW }, config())).toBe(""); });});
describe("two-clock views", () => { it("condensed: both bare ages, 'part / prompt'", () => { expect(renderLine(STALLED, compact({ view: "condensed" }))).toBe("20m / 27m"); });
it("diff (default): the fresher age plus the span between the clocks", () => { expect(renderLine(STALLED, compact())).toBe("20m + 7m"); expect(renderLine(STALLED, config())).toBe("20.0m + 7.0m"); });
it("diff drops a span that reads as zero", () => { const snap: ActivitySnapshot = { nowMs: NOW, promptAtMs: NOW - 5 * MIN, partAtMs: NOW - 5 * MIN }; expect(renderLine(snap, compact())).toBe("5m"); expect(renderLine(snap, config())).toBe("5.0m"); });
it("expanded: labeled text", () => { expect(renderLine(STALLED, compact({ view: "expanded" }))).toBe("part 20m ago · prompt 27m ago"); expect(renderLine(STALLED, config({ view: "expanded" }))).toBe("part 20.0m ago · prompt 27.0m ago"); });
it("appends outcome markers in every mode", () => { const snap = { ...STALLED, outcome: "failed" as const }; expect(renderLine(snap, compact())).toBe("20m + 7m ✗"); expect(renderLine(snap, compact({ view: "condensed" }))).toBe("20m / 27m ✗"); expect(renderLine(snap, compact({ view: "expanded" }))).toBe("part 20m ago · prompt 27m ago ✗"); });
it("shows real ages while working once the part clock is known", () => { expect(renderLine({ ...STALLED, working: true }, compact())).toBe("20m + 7m"); });
it("reads 'now' for a fresh part clock", () => { const snap: ActivitySnapshot = { nowMs: NOW, promptAtMs: NOW - 27 * MIN, partAtMs: NOW - 500 }; // the span is 27m - 500ms, which compact floors to 26m expect(renderLine(snap, compact())).toBe("now + 26m"); expect(renderLine(snap, compact({ view: "condensed" }))).toBe("now / 27m"); });
it("renders wall-clock times for the clock style", () => { const part = new Date(2026, 7, 20, 14, 5).getTime(); const prompt = new Date(2026, 7, 20, 13, 38).getTime(); const now = new Date(2026, 7, 20, 20, 0).getTime(); const snap: ActivitySnapshot = { nowMs: now, promptAtMs: prompt, partAtMs: part }; expect(renderLine(snap, config({ formatStyle: "clock", view: "condensed" }))).toBe("14:05 / 13:38"); // a span has no wall-clock form: compact fallback (5h55m part age, 27m span) expect(renderLine(snap, config({ formatStyle: "clock" }))).toBe("5h + 27m"); expect(renderLine(snap, config({ formatStyle: "clock", view: "expanded" }))).toBe("part 14:05 · prompt 13:38"); });});
describe("renderLine truncation", () => { it("passes short lines through untouched", () => { expect(renderLine({ nowMs: NOW, settledAtMs: NOW - 5 * MIN }, config(), 32)).toBe("5.0m ago"); expect(renderLine(STALLED, compact(), 32)).toBe("20m + 7m"); });
it("truncates with an ellipsis at the max width", () => { const snap: ActivitySnapshot = { nowMs: NOW, settledAtMs: NOW - 5 * MIN }; expect(renderLine(snap, config({ formatStyle: "verbose" }), 4)).toBe("5m…"); expect(renderLine(snap, config(), 1)).toBe("…"); const long = renderLine(STALLED, compact({ view: "expanded" }), 10); expect(long.endsWith("…")).toBe(true); expect(long.length).toBeLessThanOrEqual(10); });});