The pill drew the most urgent state of the fan-out next to the size of the fan-out. Two questions, one pill, 8pt apart: a spinning ring beside "2 subagents" reads as two agents running when one is. The mark is the thing the eye lands on, so its state gets read onto whatever number is next to it. While anything is happening the pill now names that state and counts the children in it — "1 running", "2 failed" — and falls back to naming the total once nothing is. The total is a tap away in the panel, and the running count is the number worth glancing at anyway. Label and mark are built by one function so they cannot disagree; the call site takes both or neither. Every non-done bucket gets copy, not just the two that are reachable today, because needs_input and attention become reachable the moment a subagent row carries requiresAttention, and the failure mode is silently the bug this commit fixes.
280 lines
9.2 KiB
TypeScript
280 lines
9.2 KiB
TypeScript
import { beforeAll, describe, expect, it } from "vitest";
|
|
import { i18n } from "@/i18n/i18next";
|
|
import type { PaseoSubagentRow, ProviderSubagentRow, SubagentRow } from "./select";
|
|
import {
|
|
buildSubagentPillPresentation,
|
|
buildSubagentRowPresentationData,
|
|
countFinishedSubagents,
|
|
resolveRowLabel,
|
|
} from "./track-presentation";
|
|
|
|
function row(
|
|
overrides: Partial<PaseoSubagentRow> & Pick<PaseoSubagentRow, "id">,
|
|
): PaseoSubagentRow {
|
|
return {
|
|
kind: "paseo",
|
|
id: overrides.id,
|
|
provider: overrides.provider ?? "codex",
|
|
title: overrides.title ?? `Agent ${overrides.id}`,
|
|
description: null,
|
|
subtitle: null,
|
|
status: overrides.status ?? "idle",
|
|
requiresAttention: overrides.requiresAttention ?? false,
|
|
createdAt: overrides.createdAt ?? new Date("2026-04-20T00:00:00.000Z"),
|
|
};
|
|
}
|
|
|
|
describe("buildSubagentPillPresentation", () => {
|
|
// The real instance, so a label that names a key nobody added renders as that key and fails.
|
|
beforeAll(async () => {
|
|
if (!i18n.isInitialized) {
|
|
await i18n.init();
|
|
}
|
|
await i18n.changeLanguage("en");
|
|
});
|
|
|
|
const pill = (rows: SubagentRow[]) => buildSubagentPillPresentation(i18n.t, rows);
|
|
|
|
it("counts the children that are running, not the fan-out", () => {
|
|
expect(pill([row({ id: "a" }), row({ id: "b", status: "running" })])).toEqual({
|
|
label: "1 running",
|
|
statusBucket: "running",
|
|
});
|
|
});
|
|
|
|
it("counts every child in the state it reports", () => {
|
|
expect(
|
|
pill([
|
|
row({ id: "a", status: "running" }),
|
|
row({ id: "b", status: "running" }),
|
|
row({ id: "c" }),
|
|
]),
|
|
).toEqual({ label: "2 running", statusBucket: "running" });
|
|
});
|
|
|
|
it("ranks a failed child above a running one, and counts the failures", () => {
|
|
expect(
|
|
pill([
|
|
row({ id: "a", status: "running" }),
|
|
row({ id: "b", status: "error", requiresAttention: true }),
|
|
row({ id: "c", status: "error" }),
|
|
]),
|
|
).toEqual({ label: "2 failed", statusBucket: "failed" });
|
|
});
|
|
|
|
it("names what it opens once every child is done", () => {
|
|
expect(pill([row({ id: "a" }), row({ id: "b" })])).toEqual({
|
|
label: "2 subagents",
|
|
statusBucket: null,
|
|
});
|
|
});
|
|
|
|
it("keeps the singular for a lone child", () => {
|
|
expect(pill([row({ id: "a" })])).toEqual({ label: "1 subagent", statusBucket: null });
|
|
});
|
|
|
|
it("has nothing to mark without rows", () => {
|
|
expect(pill([])).toEqual({ label: "0 subagents", statusBucket: null });
|
|
});
|
|
});
|
|
|
|
describe("countFinishedSubagents", () => {
|
|
it("counts eligible managed and terminal provider-owned children", () => {
|
|
const providerRows: SubagentRow[] = [
|
|
{
|
|
kind: "provider",
|
|
id: "native-running",
|
|
parentAgentId: "parent",
|
|
provider: "claude",
|
|
title: "running",
|
|
description: null,
|
|
subtitle: null,
|
|
status: "running",
|
|
requiresAttention: false,
|
|
createdAt: new Date("2026-04-20T00:00:00.000Z"),
|
|
},
|
|
{
|
|
kind: "provider",
|
|
id: "native-failed",
|
|
parentAgentId: "parent",
|
|
provider: "claude",
|
|
title: "failed",
|
|
description: null,
|
|
subtitle: null,
|
|
status: "failed",
|
|
requiresAttention: true,
|
|
createdAt: new Date("2026-04-20T00:00:01.000Z"),
|
|
},
|
|
];
|
|
|
|
expect(
|
|
countFinishedSubagents([
|
|
row({ id: "managed-running", status: "running" }),
|
|
row({ id: "managed-idle", status: "idle" }),
|
|
...providerRows,
|
|
]),
|
|
).toBe(2);
|
|
});
|
|
|
|
it("excludes running and initializing managed children", () => {
|
|
expect(
|
|
countFinishedSubagents([
|
|
row({ id: "running", status: "running" }),
|
|
row({ id: "initializing", status: "initializing" }),
|
|
row({ id: "finished", status: "idle" }),
|
|
]),
|
|
).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("resolveRowLabel", () => {
|
|
it("returns null when title is not a string", () => {
|
|
expect(resolveRowLabel(null as unknown as SubagentRow["title"])).toBe(null);
|
|
});
|
|
|
|
it("returns null for whitespace-only titles", () => {
|
|
expect(resolveRowLabel(" ")).toBe(null);
|
|
});
|
|
|
|
it("returns null for the placeholder 'new agent' regardless of case", () => {
|
|
expect(resolveRowLabel("new agent")).toBe(null);
|
|
expect(resolveRowLabel("New Agent")).toBe(null);
|
|
expect(resolveRowLabel(" NEW AGENT ")).toBe(null);
|
|
});
|
|
|
|
it("returns the trimmed title for real names", () => {
|
|
expect(resolveRowLabel(" Build the thing ")).toBe("Build the thing");
|
|
});
|
|
});
|
|
|
|
describe("buildSubagentRowPresentationData", () => {
|
|
it("namespaces the key with a subagent prefix", () => {
|
|
expect(buildSubagentRowPresentationData(row({ id: "child-a" })).key).toBe(
|
|
"paseo_subagent_child-a",
|
|
);
|
|
});
|
|
|
|
it("marks the row ready when the title resolves to a real label", () => {
|
|
const presentation = buildSubagentRowPresentationData(row({ id: "a", title: "Build it" }));
|
|
expect(presentation.titleState).toBe("ready");
|
|
expect(presentation.label).toBe("Build it");
|
|
});
|
|
|
|
it("marks the row loading and blanks the label for the placeholder title", () => {
|
|
const presentation = buildSubagentRowPresentationData(row({ id: "a", title: "new agent" }));
|
|
expect(presentation.titleState).toBe("loading");
|
|
expect(presentation.label).toBe("");
|
|
});
|
|
|
|
it("maps a running row to the running status bucket so callers render the synced loader", () => {
|
|
expect(buildSubagentRowPresentationData(row({ id: "a", status: "running" })).statusBucket).toBe(
|
|
"running",
|
|
);
|
|
});
|
|
|
|
it("maps an idle row to the done status bucket so callers render the static provider icon", () => {
|
|
expect(buildSubagentRowPresentationData(row({ id: "a", status: "idle" })).statusBucket).toBe(
|
|
"done",
|
|
);
|
|
});
|
|
|
|
it("ignores requiresAttention on the source row when computing the bucket", () => {
|
|
expect(
|
|
buildSubagentRowPresentationData(row({ id: "a", status: "idle", requiresAttention: true }))
|
|
.statusBucket,
|
|
).toBe("done");
|
|
});
|
|
});
|
|
|
|
describe("buildSubagentRowPresentationData for provider rows", () => {
|
|
function providerRow(overrides: Partial<ProviderSubagentRow> = {}): ProviderSubagentRow {
|
|
return {
|
|
kind: "provider",
|
|
id: overrides.id ?? "toolu_1",
|
|
parentAgentId: "parent",
|
|
provider: "claude",
|
|
title: "title" in overrides ? (overrides.title ?? null) : "general-purpose",
|
|
description: overrides.description ?? null,
|
|
subtitle: overrides.subtitle ?? null,
|
|
status: overrides.status ?? "running",
|
|
requiresAttention: false,
|
|
createdAt: overrides.createdAt ?? new Date("2026-07-26T00:00:00.000Z"),
|
|
};
|
|
}
|
|
|
|
it("names the row after the task and demotes the subagent type", () => {
|
|
const presentation = buildSubagentRowPresentationData(
|
|
providerRow({ title: "general-purpose", description: "Reply with banana" }),
|
|
);
|
|
expect(presentation.label).toBe("Reply with banana");
|
|
expect(presentation.subtitle).toBe("general-purpose");
|
|
});
|
|
|
|
it("tells two siblings of the same type apart", () => {
|
|
const left = buildSubagentRowPresentationData(
|
|
providerRow({ id: "a", description: "Summarize the docs" }),
|
|
);
|
|
const right = buildSubagentRowPresentationData(
|
|
providerRow({ id: "b", description: "Reply with banana" }),
|
|
);
|
|
expect(left.label).not.toBe(right.label);
|
|
});
|
|
|
|
it("keeps type-as-label and an empty subtitle when a provider reports no task", () => {
|
|
const presentation = buildSubagentRowPresentationData(
|
|
providerRow({ title: "Provider child", description: null }),
|
|
);
|
|
expect(presentation.label).toBe("Provider child");
|
|
expect(presentation.subtitle).toBe("");
|
|
});
|
|
|
|
it("stays in the loading state when neither field is known", () => {
|
|
const presentation = buildSubagentRowPresentationData(
|
|
providerRow({ title: null, description: null }),
|
|
);
|
|
expect(presentation.titleState).toBe("loading");
|
|
});
|
|
|
|
it("leaves managed subagent rows with no subtitle", () => {
|
|
expect(buildSubagentRowPresentationData(row({ id: "a", title: "Managed" })).subtitle).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("provider-owned row subtitles", () => {
|
|
function providerRow(overrides: Partial<ProviderSubagentRow> = {}): ProviderSubagentRow {
|
|
return {
|
|
kind: "provider",
|
|
id: "toolu_1",
|
|
parentAgentId: "parent",
|
|
provider: "claude",
|
|
title: "general-purpose",
|
|
description: "Reply with banana",
|
|
subtitle: null,
|
|
status: "running",
|
|
requiresAttention: false,
|
|
createdAt: new Date("2026-07-26T00:00:00.000Z"),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
it("displays provider context without interpreting it", () => {
|
|
expect(
|
|
buildSubagentRowPresentationData(
|
|
providerRow({ subtitle: "general-purpose · Opus 5 · High · 16.5k tokens" }),
|
|
).subtitle,
|
|
).toBe("general-purpose · Opus 5 · High · 16.5k tokens");
|
|
});
|
|
|
|
it("falls back to the type when an older provider sends no subtitle", () => {
|
|
expect(buildSubagentRowPresentationData(providerRow()).subtitle).toBe("general-purpose");
|
|
});
|
|
|
|
it("does not duplicate the type when it is already the primary label", () => {
|
|
expect(
|
|
buildSubagentRowPresentationData(
|
|
providerRow({ description: null, subtitle: null, title: "general-purpose" }),
|
|
).subtitle,
|
|
).toBe("");
|
|
});
|
|
});
|