Files
paseo/packages/app/src/utils/agent-attention.test.ts
T
Mohamed Boudra efd6023b90 Restore live sessions immediately after app resume (#4160)
* perf(app): restore live sessions immediately after resume

Foregrounding now reconnects every host without waiting for scheduled backoff, while cached turns, timelines, agents, and workspaces remain internally consistent until authoritative catch-up arrives. Replica persistence is keyed and incremental so ordinary updates avoid whole-directory serialization.

* fix(app): preserve turn ownership through session updates

Stream turn closure now wakes queued-message delivery, while same-turn snapshots retain local cancellation state. Offline liveness remains visible until an authoritative close arrives.

* fix(app): preserve pending chat cache writes and trim resume refactor

Directory baseline replacement must preserve independently accepted timeline changes until the deferred flush. Keep the reconnect and persistence fixes while removing the session-data facade and broad read-hook migration.

* fix(app): initialize reconnect policy from current app state

Initialize visibility before host bootstrap so a background mount cannot leave reconnect enabled. Seed sidebar fixtures through the workspace owner so cache hydration preserves their rows.

* test(app): verify submission settlement across reconnect

Keep submission acknowledgement separate from turn liveness: disconnect preserves the running turn until authoritative catch-up. Wait for confirmed running chrome before exercising queued cancellation so the alternate-send shortcut cannot race turn startup.

* docs: remove cache implementation recap
2026-09-06 10:18:41 +02:00

270 lines
7.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Agent } from "@/stores/session-store";
import { pickAttentionAgent, shouldClearAgentAttention } from "@/utils/agent-attention";
function createAgent(input: Partial<Agent> & Pick<Agent, "id">): Agent {
const { id, ...rest } = input;
return {
serverId: "server-1",
id,
provider: "codex",
status: "idle",
createdAt: new Date("2026-01-01T00:00:00.000Z"),
updatedAt: new Date("2026-01-01T00:00:00.000Z"),
lastUserMessageAt: null,
lastActivityAt: new Date("2026-01-01T00:00:00.000Z"),
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
title: null,
cwd: "/repo/worktree",
model: null,
parentAgentId: null,
labels: {},
requiresAttention: false,
attentionReason: null,
attentionTimestamp: null,
...rest,
turn: rest.turn ?? { phase: "idle", cancellationRequestId: null },
};
}
describe("shouldClearAgentAttention", () => {
it("returns true only when the agent is connected and requires attention", () => {
expect(
shouldClearAgentAttention({
agentId: "agent-1",
isConnected: true,
requiresAttention: true,
attentionReason: "finished",
}),
).toBe(true);
});
it("returns false when the app is disconnected", () => {
expect(
shouldClearAgentAttention({
agentId: "agent-1",
isConnected: false,
requiresAttention: true,
attentionReason: "finished",
}),
).toBe(false);
});
it("returns false when attention is already clear", () => {
expect(
shouldClearAgentAttention({
agentId: "agent-1",
isConnected: true,
requiresAttention: false,
attentionReason: null,
}),
).toBe(false);
});
it("returns false for empty agent ids", () => {
expect(
shouldClearAgentAttention({
agentId: "",
isConnected: true,
requiresAttention: true,
attentionReason: "finished",
}),
).toBe(false);
});
it("returns false for permission-shaped attention", () => {
expect(
shouldClearAgentAttention({
agentId: "agent-1",
isConnected: true,
requiresAttention: true,
attentionReason: "permission",
}),
).toBe(false);
});
it("returns false for focus entry when clear was deferred while already focused", () => {
expect(
shouldClearAgentAttention({
agentId: "agent-1",
isConnected: true,
requiresAttention: true,
attentionReason: "finished",
trigger: "focus-entry",
hasDeferredFocusEntryClear: true,
}),
).toBe(false);
});
it("returns true for explicit follow-up entrypoints after deferred focus clear", () => {
expect(
shouldClearAgentAttention({
agentId: "agent-1",
isConnected: true,
requiresAttention: true,
attentionReason: "finished",
trigger: "input-focus",
hasDeferredFocusEntryClear: true,
}),
).toBe(true);
expect(
shouldClearAgentAttention({
agentId: "agent-1",
isConnected: true,
requiresAttention: true,
attentionReason: "finished",
trigger: "prompt-send",
hasDeferredFocusEntryClear: true,
}),
).toBe(true);
expect(
shouldClearAgentAttention({
agentId: "agent-1",
isConnected: true,
requiresAttention: true,
attentionReason: "finished",
trigger: "agent-blur",
hasDeferredFocusEntryClear: true,
}),
).toBe(true);
});
});
describe("pickAttentionAgent", () => {
it("returns null for empty input", () => {
expect(pickAttentionAgent([])).toBeNull();
});
it("returns null when no agent requires attention", () => {
expect(
pickAttentionAgent([
createAgent({ id: "agent-1", requiresAttention: false, attentionReason: "permission" }),
createAgent({ id: "agent-2", requiresAttention: false, attentionReason: null }),
]),
).toBeNull();
});
it("returns the single permission agent", () => {
expect(
pickAttentionAgent([
createAgent({
id: "agent-1",
requiresAttention: true,
attentionReason: "permission",
attentionTimestamp: new Date("2026-01-03T00:00:00.000Z"),
}),
]),
).toBe("agent-1");
});
it("prioritizes permission over error over finished", () => {
expect(
pickAttentionAgent([
createAgent({
id: "finished-agent",
requiresAttention: true,
attentionReason: "finished",
attentionTimestamp: new Date("2026-01-01T00:00:00.000Z"),
}),
createAgent({
id: "error-agent",
requiresAttention: true,
attentionReason: "error",
attentionTimestamp: new Date("2026-01-02T00:00:00.000Z"),
}),
createAgent({
id: "permission-agent",
requiresAttention: true,
attentionReason: "permission",
attentionTimestamp: new Date("2026-01-03T00:00:00.000Z"),
}),
]),
).toBe("permission-agent");
});
it("breaks ties by oldest attention timestamp", () => {
expect(
pickAttentionAgent([
createAgent({
id: "newer-permission-agent",
requiresAttention: true,
attentionReason: "permission",
attentionTimestamp: new Date("2026-01-03T00:00:00.000Z"),
}),
createAgent({
id: "older-permission-agent",
requiresAttention: true,
attentionReason: "permission",
attentionTimestamp: new Date("2026-01-01T00:00:00.000Z"),
}),
]),
).toBe("older-permission-agent");
});
it("ignores subagents even when they require attention", () => {
expect(
pickAttentionAgent([
createAgent({
id: "subagent",
parentAgentId: "parent-agent",
requiresAttention: true,
attentionReason: "permission",
attentionTimestamp: new Date("2026-01-01T00:00:00.000Z"),
}),
createAgent({
id: "top-level-agent",
requiresAttention: true,
attentionReason: "finished",
attentionTimestamp: new Date("2026-01-02T00:00:00.000Z"),
}),
]),
).toBe("top-level-agent");
});
it("returns null when only subagents require attention", () => {
expect(
pickAttentionAgent([
createAgent({
id: "subagent",
parentAgentId: "parent-agent",
requiresAttention: true,
attentionReason: "permission",
attentionTimestamp: new Date("2026-01-01T00:00:00.000Z"),
}),
]),
).toBeNull();
});
it("ignores agents with requiresAttention true but null reason", () => {
expect(
pickAttentionAgent([
createAgent({
id: "ignored-agent",
requiresAttention: true,
attentionReason: null,
attentionTimestamp: new Date("2026-01-01T00:00:00.000Z"),
}),
createAgent({
id: "error-agent",
requiresAttention: true,
attentionReason: "error",
attentionTimestamp: new Date("2026-01-02T00:00:00.000Z"),
}),
]),
).toBe("error-agent");
});
});