blamouche_obsidian-any-ai-code/tests/session-utils.test.ts
blamouche 560735b898 Activity-aware tab dot + idle auto-close for automation sessions
The session tab status dot now reflects live CLI activity rather than just
whether the process is alive: green = a manual session's AI is working,
purple = an automation session's AI is working, gray = the CLI is idle
(finished its turn) or stopped. CliSession tracks activity continuously —
output flowing = working, quiet for ~5s = idle — and notifies the view via
onActivityChange to repaint the dot.

Add a new opt-in setting "Auto-close automation sessions when idle": closes
an automation tab once its CLI goes quiet after the prompt ran, even if the
process stays interactive (Claude/Codex). It complements the existing
on-exit close (renamed "...on exit"); both are kept. Idle-close is armed only
after the prompt is sent and driven by real CLI output, so boot and slow
first responses don't close the tab early.

Extract the pure tabDotClass helper to session-utils.ts with tests. Update
README. Bump to 0.2.11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:40:03 +02:00

110 lines
3.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
canOpenSession,
nextSessionLabel,
resolveRuntimeForAutomation,
runtimeMatches,
tabDotClass
} from "../session-utils";
import type { CliRuntimeConfig } from "../runtime-utils";
const runtimes: CliRuntimeConfig[] = [
{ id: "claude", name: "Claude", command: "claude" },
{ id: "codex", name: "Codex", command: "codex" },
{ id: "abc123", name: "My Gemini", command: "gemini" }
];
describe("runtimeMatches", () => {
it("matches by id", () => {
expect(runtimeMatches(runtimes[0], "claude")).toBe(true);
});
it("matches by name case-insensitively", () => {
expect(runtimeMatches(runtimes[2], "my gemini")).toBe(true);
});
it("returns false on no match or empty target", () => {
expect(runtimeMatches(runtimes[0], "codex")).toBe(false);
expect(runtimeMatches(runtimes[0], " ")).toBe(false);
});
});
describe("resolveRuntimeForAutomation", () => {
it("resolves a declared runtime by name", () => {
expect(resolveRuntimeForAutomation(runtimes, "Codex", "claude")?.id).toBe("codex");
});
it("resolves a declared runtime by id", () => {
expect(resolveRuntimeForAutomation(runtimes, "abc123", "claude")?.id).toBe("abc123");
});
it("returns null when a declared runtime is unknown", () => {
expect(resolveRuntimeForAutomation(runtimes, "Nope", "claude")).toBeNull();
});
it("falls back to the default runtime when none is declared", () => {
expect(resolveRuntimeForAutomation(runtimes, null, "codex")?.id).toBe("codex");
expect(resolveRuntimeForAutomation(runtimes, "", "abc123")?.id).toBe("abc123");
});
it("falls back to the first runtime when the default id is invalid", () => {
expect(resolveRuntimeForAutomation(runtimes, null, "missing")?.id).toBe("claude");
});
it("returns null when there are no runtimes", () => {
expect(resolveRuntimeForAutomation([], null, "claude")).toBeNull();
});
});
describe("nextSessionLabel", () => {
it("uses the runtime name when free", () => {
expect(nextSessionLabel([], "Claude")).toBe("Claude");
});
it("disambiguates duplicates", () => {
expect(nextSessionLabel(["Claude"], "Claude")).toBe("Claude (2)");
expect(nextSessionLabel(["Claude", "Claude (2)"], "Claude")).toBe("Claude (3)");
});
it("falls back to (Unnamed) for blank names", () => {
expect(nextSessionLabel([], " ")).toBe("(Unnamed)");
});
});
describe("canOpenSession", () => {
it("allows when under the cap", () => {
expect(canOpenSession(2, 8)).toBe(true);
});
it("blocks at the cap", () => {
expect(canOpenSession(8, 8)).toBe(false);
});
it("treats 0 or negative as unlimited", () => {
expect(canOpenSession(100, 0)).toBe(true);
expect(canOpenSession(100, -1)).toBe(true);
});
});
describe("tabDotClass", () => {
it("is green for a working manual session", () => {
expect(tabDotClass({ running: true, activity: "working", origin: "manual" })).toBe("is-working");
});
it("is purple for a working automation session", () => {
expect(tabDotClass({ running: true, activity: "working", origin: "automation" })).toBe(
"is-automation"
);
});
it("is gray when idle, regardless of origin", () => {
expect(tabDotClass({ running: true, activity: "idle", origin: "manual" })).toBe("is-idle");
expect(tabDotClass({ running: true, activity: "idle", origin: "automation" })).toBe("is-idle");
});
it("is gray when the process has stopped even if last activity was working", () => {
expect(tabDotClass({ running: false, activity: "working", origin: "automation" })).toBe(
"is-idle"
);
});
});