Files
paseo/packages/app/src/subagents/select.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

438 lines
12 KiB
TypeScript

import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { afterEach, describe, expect, it } from "vitest";
import { selectProviderSubagentsForParent, selectSubagentsForParent } from "./select";
import { useProviderSubagentStore } from "./provider-store";
import { useSessionStore, type Agent } from "@/stores/session-store";
const SERVER_ID = "server-1";
const AGENT_TIMESTAMP = new Date("2026-03-08T10:00:00.000Z");
const EMPTY_PENDING_ARCHIVE_IDS = new Set<string>();
const AGENT_DEFAULTS: Agent = {
serverId: SERVER_ID,
id: "agent",
provider: "codex",
status: "idle",
turn: { phase: "idle", cancellationRequestId: null },
createdAt: AGENT_TIMESTAMP,
updatedAt: AGENT_TIMESTAMP,
lastUserMessageAt: null,
lastActivityAt: AGENT_TIMESTAMP,
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
runtimeInfo: undefined,
lastUsage: undefined,
lastError: null,
title: "Agent",
cwd: "/tmp/project",
model: null,
features: undefined,
thinkingOptionId: undefined,
requiresAttention: false,
attentionReason: null,
attentionTimestamp: null,
archivedAt: null,
parentAgentId: null,
labels: {},
projectPlacement: null,
};
function makeAgent(input: Partial<Agent> & Pick<Agent, "id">): Agent {
return { ...AGENT_DEFAULTS, ...input };
}
function setAgents(agents: Agent[]): void {
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
useSessionStore
.getState()
.setAgents(SERVER_ID, new Map(agents.map((agent) => [agent.id, agent])));
}
afterEach(() => {
useSessionStore.getState().clearSession(SERVER_ID);
useProviderSubagentStore.setState({
descriptors: new Map(),
timelines: new Map(),
hiddenFromTrack: new Set(),
});
});
describe("selectSubagentsForParent", () => {
it("hides cached provider children when the host does not support them", () => {
useProviderSubagentStore.getState().applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
id: "provider-child",
parentAgentId: "parent-a",
provider: "codex",
title: "Provider child",
description: null,
subtitle: "Codex worker · 4.2k tokens",
status: "completed",
createdAt: "2026-03-08T10:01:00.000Z",
updatedAt: "2026-03-08T10:02:00.000Z",
toolCallId: "call-1",
},
});
const params = { serverId: SERVER_ID, parentAgentId: "parent-a" };
expect(
selectProviderSubagentsForParent(useProviderSubagentStore.getState(), params, false),
).toEqual([]);
expect(
selectProviderSubagentsForParent(useProviderSubagentStore.getState(), params, true).map(
(row) => row.id,
),
).toEqual(["provider-child"]);
expect(
selectProviderSubagentsForParent(useProviderSubagentStore.getState(), params, true)[0]
?.subtitle,
).toBe("Codex worker · 4.2k tokens");
});
it("hides locally dismissed provider children while retaining their descriptor", () => {
const store = useProviderSubagentStore.getState();
store.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
id: "provider-child",
parentAgentId: "parent-a",
provider: "codex",
title: "Provider child",
description: null,
status: "completed",
createdAt: "2026-03-08T10:01:00.000Z",
updatedAt: "2026-03-08T10:02:00.000Z",
toolCallId: "call-1",
},
});
store.hideFromTrack(SERVER_ID, "parent-a", ["provider-child"]);
expect(
selectProviderSubagentsForParent(
useProviderSubagentStore.getState(),
{ serverId: SERVER_ID, parentAgentId: "parent-a" },
true,
),
).toEqual([]);
expect(useProviderSubagentStore.getState().descriptors.size).toBe(1);
});
it("places nested provider children only beneath their direct provider parent", () => {
const store = useProviderSubagentStore.getState();
const base = {
parentAgentId: "parent-a",
provider: "claude" as const,
title: "general-purpose",
subtitle: null,
status: "running" as const,
createdAt: "2026-09-04T10:00:00.000Z",
updatedAt: "2026-09-04T10:00:00.000Z",
toolCallId: null,
};
store.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: { ...base, id: "direct", description: "Direct", parentSubagentId: null },
});
store.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
...base,
id: "nested",
description: "Nested",
parentSubagentId: "direct",
},
});
expect(
selectProviderSubagentsForParent(
useProviderSubagentStore.getState(),
{ serverId: SERVER_ID, parentAgentId: "parent-a" },
true,
true,
).map((row) => row.id),
).toEqual(["direct"]);
expect(
selectProviderSubagentsForParent(
useProviderSubagentStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-a",
providerParentSubagentId: "direct",
},
true,
true,
).map((row) => row.id),
).toEqual(["nested"]);
});
it("returns only non-archived children for the requested parent", () => {
setAgents([
makeAgent({ id: "parent-a" }),
makeAgent({ id: "child-a", parentAgentId: "parent-a" }),
makeAgent({
id: "archived-child",
parentAgentId: "parent-a",
archivedAt: new Date("2026-03-08T12:00:00.000Z"),
}),
]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-a",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
});
it("excludes siblings, unrelated agents, and grandchildren", () => {
setAgents([
makeAgent({ id: "parent-a" }),
makeAgent({ id: "parent-b" }),
makeAgent({ id: "child-a", parentAgentId: "parent-a" }),
makeAgent({ id: "sibling-b", parentAgentId: "parent-b" }),
makeAgent({ id: "grandchild-a", parentAgentId: "child-a" }),
makeAgent({ id: "unrelated" }),
]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-a",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
});
it("shows only direct children for each parent", () => {
setAgents([
makeAgent({ id: "parent" }),
makeAgent({ id: "child", parentAgentId: "parent" }),
makeAgent({ id: "grandchild", parentAgentId: "child" }),
]);
const parentRows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
const childRows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "child",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
expect(parentRows.map((row) => row.id)).toEqual(["child"]);
expect(childRows.map((row) => row.id)).toEqual(["grandchild"]);
});
it("sorts by createdAt ascending", () => {
setAgents([
makeAgent({ id: "parent" }),
makeAgent({
id: "third",
parentAgentId: "parent",
createdAt: new Date("2026-03-08T10:03:00.000Z"),
}),
makeAgent({
id: "first",
parentAgentId: "parent",
createdAt: new Date("2026-03-08T10:01:00.000Z"),
}),
makeAgent({
id: "second",
parentAgentId: "parent",
createdAt: new Date("2026-03-08T10:02:00.000Z"),
}),
]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
expect(rows.map((row) => row.id)).toEqual(["first", "second", "third"]);
});
it("maps only row-rendered fields and does not expose onOpen", () => {
const createdAt = new Date("2026-03-08T10:01:00.000Z");
setAgents([
makeAgent({ id: "parent" }),
makeAgent({
id: "child",
parentAgentId: "parent",
provider: "claude",
title: "Review child",
status: "running",
requiresAttention: true,
createdAt,
model: "should-not-leak",
cwd: "/private/project",
}),
]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent",
},
EMPTY_PENDING_ARCHIVE_IDS,
);
expect(rows).toEqual([
{
kind: "paseo",
id: "child",
provider: "claude",
title: "Review child",
description: null,
subtitle: null,
status: "running",
turn: { phase: "idle", cancellationRequestId: null },
requiresAttention: true,
createdAt,
},
]);
expect(Object.keys(rows[0] ?? {}).sort()).toEqual([
"createdAt",
"description",
"id",
"kind",
"provider",
"requiresAttention",
"status",
"subtitle",
"title",
"turn",
]);
expect(rows[0]).not.toHaveProperty("onOpen");
expect(rows[0]).not.toHaveProperty("model");
expect(rows[0]).not.toHaveProperty("cwd");
});
it("moves a child when parentAgentId changes", () => {
const child = makeAgent({ id: "child", parentAgentId: "parent-a" });
setAgents([makeAgent({ id: "parent-a" }), makeAgent({ id: "parent-b" }), child]);
expect(
selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-a",
},
EMPTY_PENDING_ARCHIVE_IDS,
).map((row) => row.id),
).toEqual(["child"]);
expect(
selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-b",
},
EMPTY_PENDING_ARCHIVE_IDS,
).map((row) => row.id),
).toEqual([]);
setAgents([
makeAgent({ id: "parent-a" }),
makeAgent({ id: "parent-b" }),
{ ...child, parentAgentId: "parent-b" },
]);
expect(
selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-a",
},
EMPTY_PENDING_ARCHIVE_IDS,
).map((row) => row.id),
).toEqual([]);
expect(
selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent-b",
},
EMPTY_PENDING_ARCHIVE_IDS,
).map((row) => row.id),
).toEqual(["child"]);
});
it("excludes children whose archive is pending", () => {
setAgents([
makeAgent({ id: "parent" }),
makeAgent({ id: "child-a", parentAgentId: "parent" }),
makeAgent({ id: "child-b", parentAgentId: "parent" }),
]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent",
},
new Set(["child-b"]),
);
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
});
it("returns the shared empty array when pending archive hides the last child", () => {
setAgents([makeAgent({ id: "parent" }), makeAgent({ id: "child", parentAgentId: "parent" })]);
const rows = selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "parent",
},
new Set(["child"]),
);
expect(rows).toEqual([]);
expect(rows).toBe(
selectSubagentsForParent(
useSessionStore.getState(),
{
serverId: SERVER_ID,
parentAgentId: "missing-parent",
},
EMPTY_PENDING_ARCHIVE_IDS,
),
);
});
});