Files
paseo/packages/app/src/subagents/provider-store.test.ts
T
Mohamed BoudraandGitHub abfe955867 Show friendly native subagent names and hide finished work (#2073)
* feat(subagents): archive finished children and show native names

Keep native provider sessions intact while letting users clear completed children from the track. Persist dismissed provider child IDs so history replay does not restore them.

* fix(subagents): cover persistence and naming edge cases

* fix(subagents): preserve active descendants and native names

* fix(subagents): retain dismissed timelines and trim paths

* fix(subagents): retain history across hydration

* test(subagents): expect normalized native descriptions

* fix(subagents): separate hidden rows from retained history

* fix(subagents): preserve restored hidden tabs safely

* fix(subagents): integrate archive with run state

* test(codex): expect normalized child name

* fix(subagents): retain dismissed tabs on reload

* fix(subagents): hide finished native agents locally

Keep the header action presentation-only so it does not mutate agent lifecycle or expand the runtime protocol.

* fix(subagents): humanize native agent names

* fix(subagents): preserve names in Codex history

* fix(subagents): retain hidden rows across reloads
2026-07-15 11:58:31 +02:00

484 lines
15 KiB
TypeScript

import { afterEach, describe, expect, test } from "vitest";
import { providerSubagentKey, useProviderSubagentStore } from "./provider-store";
const SERVER_ID = "server-1";
const PARENT_ID = "parent-1";
const SUBAGENT_ID = "child-1";
afterEach(() => {
useProviderSubagentStore.setState({
descriptors: new Map(),
timelines: new Map(),
hiddenFromTrack: new Set(),
});
});
describe("provider subagent client store", () => {
test("builds a shared stream model from ordered provider updates", () => {
const subagents = useProviderSubagentStore.getState();
subagents.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
id: SUBAGENT_ID,
parentAgentId: PARENT_ID,
provider: "codex",
title: "Explore",
description: "Inspect the repository",
status: "running",
createdAt: "2026-07-12T10:00:00.000Z",
updatedAt: "2026-07-12T10:00:00.000Z",
toolCallId: "call-1",
},
});
subagents.applyUpdate(SERVER_ID, {
kind: "timeline",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
epoch: "epoch-1",
seq: 2,
timestamp: "2026-07-12T10:00:02.000Z",
item: { type: "assistant_message", text: "New live output." },
});
subagents.replaceTimeline(SERVER_ID, {
requestId: "history-1",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
direction: "tail",
epoch: "epoch-1",
reset: false,
staleCursor: false,
gap: false,
window: { minSeq: 1, maxSeq: 1, nextSeq: 2 },
hasOlder: false,
hasNewer: true,
rows: [
{
seq: 1,
timestamp: "2026-07-12T10:00:01.000Z",
item: { type: "assistant_message", text: "Older history." },
},
],
error: null,
});
const liveTimeline = useProviderSubagentStore
.getState()
.timelines.get(providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID));
subagents.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
id: SUBAGENT_ID,
parentAgentId: PARENT_ID,
provider: "codex",
title: "Explore",
description: "Inspect the repository",
status: "running",
createdAt: "2026-07-12T10:00:00.000Z",
updatedAt: "2026-07-12T10:00:01.500Z",
toolCallId: "call-1",
},
});
expect(
useProviderSubagentStore
.getState()
.timelines.get(providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID)),
).toBe(liveTimeline);
subagents.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
id: SUBAGENT_ID,
parentAgentId: PARENT_ID,
provider: "codex",
title: "Explore",
description: "Inspect the repository",
status: "completed",
createdAt: "2026-07-12T10:00:00.000Z",
updatedAt: "2026-07-12T10:00:02.000Z",
toolCallId: "call-1",
},
});
const key = providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID);
const state = useProviderSubagentStore.getState();
expect(state.descriptors.get(key)?.status).toBe("completed");
expect(state.timelines.get(key)?.head).toEqual([]);
expect(state.timelines.get(key)?.tail).toEqual([
expect.objectContaining({
kind: "assistant_message",
text: "Older history.New live output.",
}),
]);
});
test("removes timelines for children no longer returned by the provider", () => {
const store = useProviderSubagentStore.getState();
store.applyUpdate(SERVER_ID, {
kind: "timeline",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
epoch: "epoch-1",
seq: 1,
timestamp: "2026-07-12T10:00:01.000Z",
item: { type: "assistant_message", text: "Removed child output." },
});
store.replaceList(SERVER_ID, PARENT_ID, []);
expect(
useProviderSubagentStore
.getState()
.timelines.has(providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID)),
).toBe(false);
});
test("hides finished children locally without removing their timelines", () => {
const store = useProviderSubagentStore.getState();
store.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
id: SUBAGENT_ID,
parentAgentId: PARENT_ID,
provider: "codex",
title: "Finished child",
description: null,
status: "completed",
createdAt: "2026-07-12T10:00:00.000Z",
updatedAt: "2026-07-12T10:00:02.000Z",
toolCallId: "call-1",
},
});
store.applyUpdate(SERVER_ID, {
kind: "timeline",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
epoch: "epoch-1",
seq: 1,
timestamp: "2026-07-12T10:00:01.000Z",
item: { type: "assistant_message", text: "Finished output." },
});
store.hideFinishedForParent(SERVER_ID, PARENT_ID);
const state = useProviderSubagentStore.getState();
const key = providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID);
expect(state.descriptors.get(key)?.title).toBe("Finished child");
expect(state.hiddenFromTrack.has(key)).toBe(true);
expect(state.timelines.get(key)?.tail).toEqual([
expect.objectContaining({ kind: "assistant_message", text: "Finished output." }),
]);
});
test("reveals a hidden child when the provider reports it running again", () => {
const store = useProviderSubagentStore.getState();
const completed = {
id: SUBAGENT_ID,
parentAgentId: PARENT_ID,
provider: "codex" as const,
title: "Finished child",
description: null,
status: "completed" as const,
createdAt: "2026-07-12T10:00:00.000Z",
updatedAt: "2026-07-12T10:00:02.000Z",
toolCallId: "call-1",
};
store.applyUpdate(SERVER_ID, { kind: "upsert", subagent: completed });
store.hideFinishedForParent(SERVER_ID, PARENT_ID);
store.replaceList(SERVER_ID, PARENT_ID, [completed]);
const key = providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID);
expect(useProviderSubagentStore.getState().hiddenFromTrack.has(key)).toBe(true);
store.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: { ...completed, status: "running", updatedAt: "2026-07-12T10:01:00.000Z" },
});
expect(useProviderSubagentStore.getState().hiddenFromTrack.has(key)).toBe(false);
});
test("keeps hidden state when a child temporarily disappears from the provider list", () => {
const store = useProviderSubagentStore.getState();
store.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
id: SUBAGENT_ID,
parentAgentId: PARENT_ID,
provider: "codex",
title: "Finished child",
description: null,
status: "completed",
createdAt: "2026-07-12T10:00:00.000Z",
updatedAt: "2026-07-12T10:00:02.000Z",
toolCallId: "call-1",
},
});
store.hideFinishedForParent(SERVER_ID, PARENT_ID);
store.replaceList(SERVER_ID, PARENT_ID, []);
const state = useProviderSubagentStore.getState();
const key = providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID);
expect(state.descriptors.has(key)).toBe(false);
expect(state.hiddenFromTrack.has(key)).toBe(true);
});
test("keeps a finished child hidden across remove and history replay", () => {
const store = useProviderSubagentStore.getState();
const completed = {
id: SUBAGENT_ID,
parentAgentId: PARENT_ID,
provider: "codex" as const,
title: "Finished child",
description: null,
status: "completed" as const,
createdAt: "2026-07-12T10:00:00.000Z",
updatedAt: "2026-07-12T10:00:02.000Z",
toolCallId: "call-1",
};
store.applyUpdate(SERVER_ID, { kind: "upsert", subagent: completed });
store.hideFinishedForParent(SERVER_ID, PARENT_ID);
store.applyUpdate(SERVER_ID, {
kind: "remove",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
});
store.applyUpdate(SERVER_ID, { kind: "upsert", subagent: completed });
const key = providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID);
expect(useProviderSubagentStore.getState().hiddenFromTrack.has(key)).toBe(true);
});
test("applies terminal list status to a timeline received before its descriptor", () => {
const store = useProviderSubagentStore.getState();
store.applyUpdate(SERVER_ID, {
kind: "timeline",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
epoch: "epoch-1",
seq: 1,
timestamp: "2026-07-12T10:00:01.000Z",
item: { type: "assistant_message", text: "Restored output." },
});
const key = providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID);
expect(useProviderSubagentStore.getState().timelines.get(key)?.head).not.toEqual([]);
store.replaceList(SERVER_ID, PARENT_ID, [
{
id: SUBAGENT_ID,
parentAgentId: PARENT_ID,
provider: "codex",
title: "Restored child",
description: null,
status: "completed",
createdAt: "2026-07-12T10:00:00.000Z",
updatedAt: "2026-07-12T10:00:02.000Z",
toolCallId: "call-1",
},
]);
const timeline = useProviderSubagentStore.getState().timelines.get(key);
expect(timeline?.head).toEqual([]);
expect(timeline?.tail).toEqual([
expect.objectContaining({ kind: "assistant_message", text: "Restored output." }),
]);
});
test("keeps late timeline rows terminal after the descriptor completes", () => {
const store = useProviderSubagentStore.getState();
store.applyUpdate(SERVER_ID, {
kind: "upsert",
subagent: {
id: SUBAGENT_ID,
parentAgentId: PARENT_ID,
provider: "codex",
title: "Restored child",
description: null,
status: "completed",
createdAt: "2026-07-12T10:00:00.000Z",
updatedAt: "2026-07-12T10:00:02.000Z",
toolCallId: "call-1",
},
});
store.applyUpdate(SERVER_ID, {
kind: "timeline",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
epoch: "epoch-1",
seq: 1,
timestamp: "2026-07-12T10:00:01.000Z",
item: { type: "assistant_message", text: "Late restored output." },
});
const timeline = useProviderSubagentStore
.getState()
.timelines.get(providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID));
expect(timeline?.head).toEqual([]);
expect(timeline?.tail).toEqual([
expect.objectContaining({ kind: "assistant_message", text: "Late restored output." }),
]);
});
test("merges bounded older pages and tracks whether more history remains", () => {
const store = useProviderSubagentStore.getState();
store.replaceTimeline(SERVER_ID, {
requestId: "tail-page",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
direction: "tail",
epoch: "epoch-1",
reset: false,
staleCursor: false,
gap: false,
window: { minSeq: 2, maxSeq: 2, nextSeq: 3 },
hasOlder: true,
hasNewer: false,
rows: [
{
seq: 2,
timestamp: "2026-07-12T10:00:02.000Z",
item: { type: "assistant_message", text: "Recent output." },
},
],
error: null,
});
store.replaceTimeline(SERVER_ID, {
requestId: "older-page",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
direction: "before",
epoch: "epoch-1",
reset: false,
staleCursor: false,
gap: false,
window: { minSeq: 1, maxSeq: 2, nextSeq: 3 },
hasOlder: false,
hasNewer: true,
rows: [
{
seq: 1,
timestamp: "2026-07-12T10:00:01.000Z",
item: { type: "assistant_message", text: "Older output." },
},
],
error: null,
});
const timeline = useProviderSubagentStore
.getState()
.timelines.get(providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID));
expect(timeline?.hasOlder).toBe(false);
expect([...timeline!.rows.keys()]).toEqual([2, 1]);
expect(timeline?.head).toEqual([
expect.objectContaining({ kind: "assistant_message", text: "Older output.Recent output." }),
]);
});
test("ignores delayed live updates from a stale timeline epoch", () => {
const store = useProviderSubagentStore.getState();
store.replaceTimeline(SERVER_ID, {
requestId: "current-page",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
direction: "tail",
epoch: "epoch-current",
reset: true,
staleCursor: false,
gap: false,
window: { minSeq: 2, maxSeq: 2, nextSeq: 3 },
hasOlder: false,
hasNewer: false,
rows: [
{
seq: 2,
timestamp: "2026-07-12T10:00:02.000Z",
item: { type: "assistant_message", text: "Current output." },
},
],
error: null,
});
store.applyUpdate(SERVER_ID, {
kind: "timeline",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
epoch: "epoch-stale",
seq: 3,
timestamp: "2026-07-12T10:00:03.000Z",
item: { type: "assistant_message", text: "Stale output." },
});
const timeline = useProviderSubagentStore
.getState()
.timelines.get(providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID));
expect(timeline?.epoch).toBe("epoch-current");
expect([...timeline!.rows.keys()]).toEqual([2]);
expect(timeline?.head).toEqual([
expect.objectContaining({ kind: "assistant_message", text: "Current output." }),
]);
});
test("replaces cached rows with an authoritative tail page after a reconnect gap", () => {
const store = useProviderSubagentStore.getState();
store.replaceTimeline(SERVER_ID, {
requestId: "old-tail",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
direction: "tail",
epoch: "epoch-1",
reset: false,
staleCursor: false,
gap: false,
window: { minSeq: 1, maxSeq: 500, nextSeq: 501 },
hasOlder: true,
hasNewer: false,
rows: [
{
seq: 100,
timestamp: "2026-07-12T10:00:00.000Z",
item: { type: "assistant_message", text: "Old cached output." },
},
],
error: null,
});
store.replaceTimeline(SERVER_ID, {
requestId: "reconnect-tail",
parentAgentId: PARENT_ID,
subagentId: SUBAGENT_ID,
provider: "codex",
direction: "tail",
epoch: "epoch-1",
reset: false,
staleCursor: false,
gap: false,
window: { minSeq: 1, maxSeq: 500, nextSeq: 501 },
hasOlder: true,
hasNewer: false,
rows: [
{
seq: 401,
timestamp: "2026-07-12T10:00:01.000Z",
item: { type: "assistant_message", text: "Current tail output." },
},
],
error: null,
});
const timeline = useProviderSubagentStore
.getState()
.timelines.get(providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID));
expect([...timeline!.rows.keys()]).toEqual([401]);
expect(timeline?.head).toEqual([
expect.objectContaining({ kind: "assistant_message", text: "Current tail output." }),
]);
});
});