import { describe, expect, it, vi } from "vite-plus/test"; vi.mock("@expo/ui/swift-ui", () => ({ HStack: "HStack", Image: "Image", Spacer: "Spacer", Text: "Text", VStack: "VStack", ZStack: "ZStack",})); vi.mock("@expo/ui/swift-ui/modifiers", () => ({ font: (value: unknown) => value, foregroundStyle: (value: unknown) => value, frame: (value: unknown) => value, layoutPriority: (value: unknown) => value, lineLimit: (value: unknown) => value, padding: (value: unknown) => value, resizable: (value: unknown) => value, widgetURL: (value: unknown) => ({ widgetURL: value }),})); vi.mock("expo-widgets", () => ({ createLiveActivity: vi.fn((name: string, layout: unknown) => ({ layout, name })),})); import { AgentActivity, type AgentActivityProps, type AgentActivityRowProps,} from "./AgentActivity"; function makeRow(overrides: Partial<AgentActivityRowProps>): AgentActivityRowProps { return { environmentId: "env-1", threadId: "thread-1", projectTitle: "Project", threadTitle: "Thread", modelTitle: "gpt-5.4", phase: "running", status: "Working", updatedAt: "2026-05-25T13:07:00.000Z", deepLink: "/threads/env-1/thread-1", ...overrides, };} const props = { title: "T3 Code", subtitle: "Agent work in progress", activeCount: 1, updatedAt: "2026-05-25T13:07:00.000Z", activities: [],} satisfies AgentActivityProps; const environment = { colorScheme: "dark", isLuminanceReduced: false,} as const; const lightEnvironment = { colorScheme: "light", isLuminanceReduced: false,} as const; describe("AgentActivity widget layout", () => { it("tints each row by its own phase using the web sidebar's dark palette", () => { const layout = AgentActivity( { ...props, activeCount: 2, activities: [ makeRow({}), makeRow({ threadId: "thread-2", phase: "waiting_for_approval", status: "Approval" }), ], }, environment as never, ); const banner = JSON.stringify(layout.banner); expect(banner).toContain("#7dd3fc"); // sky-300: running expect(banner).toContain("#fcd34d"); // amber-300: waiting_for_approval }); it("switches to the web sidebar's light palette when the scheme is light", () => { // macOS (iPhone Mirroring / Mac notification center) renders the activity // on a light background; the dark-material palette is illegible there. const layout = AgentActivity( { ...props, activeCount: 2, activities: [ makeRow({}), makeRow({ threadId: "thread-2", phase: "waiting_for_approval", status: "Approval" }), ], }, lightEnvironment as never, ); const banner = JSON.stringify(layout.banner); expect(banner).toContain("#0284c7"); // sky-600: running expect(banner).toContain("#d97706"); // amber-600: waiting_for_approval expect(banner).not.toContain("#7dd3fc"); expect(banner).not.toContain("#fcd34d"); }); it("orders rows attention-first in the banner", () => { const layout = AgentActivity( { ...props, activeCount: 2, activities: [ makeRow({ threadTitle: "Working thread" }), makeRow({ threadId: "thread-2", threadTitle: "Blocked thread", phase: "waiting_for_approval", status: "Approval", }), ], }, environment as never, ); const banner = JSON.stringify(layout.banner); expect(banner.indexOf("Blocked thread")).toBeGreaterThan(-1); expect(banner.indexOf("Blocked thread")).toBeLessThan(banner.indexOf("Working thread")); }); it("summarizes the attention count in the banner header", () => { const layout = AgentActivity( { ...props, activeCount: 3, activities: [ makeRow({}), makeRow({ threadId: "thread-2", phase: "waiting_for_input", status: "Input" }), ], }, environment as never, ); const banner = JSON.stringify(layout.banner); expect(banner).toContain("3 active agents"); expect(banner).toContain("1 needs attention"); }); it("uses the attention tint for the compact presentations when a row needs input", () => { const layout = AgentActivity( { ...props, activeCount: 2, activities: [ makeRow({}), makeRow({ threadId: "thread-2", phase: "waiting_for_input", status: "Input" }), ], }, environment as never, ); expect(JSON.stringify(layout.compactLeading)).toContain("#a5b4fc"); // indigo-300 expect(JSON.stringify(layout.compactTrailing)).toContain("Input"); expect(JSON.stringify(layout.minimal)).toContain("#a5b4fc"); }); it("deep links the banner to the row that needs attention", () => { const layout = AgentActivity( { ...props, activeCount: 2, activities: [ makeRow({}), makeRow({ threadId: "thread-2", phase: "waiting_for_approval", status: "Approval", deepLink: "/threads/env-1/thread-2", }), ], }, environment as never, ); expect(JSON.stringify(layout.banner)).toContain( '"widgetURL":"t3code://threads/env-1/thread-2"', ); }); it("deep links the banner to the first row when nothing needs attention", () => { const layout = AgentActivity({ ...props, activities: [makeRow({})] }, environment as never); expect(JSON.stringify(layout.banner)).toContain( '"widgetURL":"t3code://threads/env-1/thread-1"', ); }); it("omits the deep link for unsafe paths and empty aggregates", () => { expect(JSON.stringify(AgentActivity(props, environment as never))).not.toContain("widgetURL"); expect( JSON.stringify( AgentActivity( { ...props, activities: [makeRow({ deepLink: "//evil.example" })] }, environment as never, ), ), ).not.toContain("widgetURL"); }); it("leads with the outcome instead of a zero count when nothing is active", () => { const layout = AgentActivity( { ...props, subtitle: "Agent work completed", activeCount: 0, activities: [makeRow({ phase: "completed", status: "Done" })], }, environment as never, ); const banner = JSON.stringify(layout.banner); expect(banner).toContain("Agent work completed"); expect(banner).not.toContain("0 active"); expect(banner).toContain("#6ee7b7"); // emerald-300 header tint expect(JSON.stringify(layout.compactTrailing)).toContain("Done"); expect(JSON.stringify(layout.compactTrailing)).not.toContain("0 active"); expect(JSON.stringify(layout.expandedLeading)).toContain("Done"); expect(JSON.stringify(layout.minimal)).toContain("checkmark.circle.fill"); expect(JSON.stringify(layout.bannerSmall)).toContain("Done"); }); it("reads Failed when the finished work ended in failure", () => { const layout = AgentActivity( { ...props, subtitle: "Agent work failed", activeCount: 0, activities: [makeRow({ phase: "failed", status: "Failed" })], }, environment as never, ); const banner = JSON.stringify(layout.banner); expect(banner).toContain("Agent work failed"); expect(banner).toContain("#fca5a5"); // red-300 header tint expect(JSON.stringify(layout.compactTrailing)).toContain("Failed"); expect(JSON.stringify(layout.expandedLeading)).toContain("Failed"); expect(JSON.stringify(layout.minimal)).toContain("xmark.octagon.fill"); }); it("lets a failure dominate mixed finished outcomes across every presentation", () => { const layout = AgentActivity( { ...props, // The server subtitle keys off the newest terminal row (completed // here); the layout must still read Failed everywhere so the header // text never disagrees with the tint, count slots, or minimal glyph. subtitle: "Agent work completed", activeCount: 0, activities: [ makeRow({ phase: "completed", status: "Done" }), makeRow({ threadId: "thread-2", phase: "failed", status: "Failed" }), ], }, environment as never, ); const banner = JSON.stringify(layout.banner); expect(banner).toContain("Agent work failed"); expect(banner).not.toContain("Agent work completed"); expect(banner).toContain("#fca5a5"); // red-300 header tint expect(JSON.stringify(layout.compactTrailing)).toContain("Failed"); expect(JSON.stringify(layout.expandedLeading)).toContain("Failed"); expect(JSON.stringify(layout.minimal)).toContain("xmark.octagon.fill"); }); it("renders up to five rows in the banner", () => { const layout = AgentActivity( { ...props, activeCount: 6, activities: [1, 2, 3, 4, 5, 6].map((n) => makeRow({ threadId: `t${n}`, threadTitle: `Thread ${n}` }), ), }, environment as never, ); const banner = JSON.stringify(layout.banner); for (const visible of [1, 2, 3, 4, 5]) { expect(banner).toContain(`Thread ${visible}`); } expect(banner).not.toContain("Thread 6"); });});
import { describe, expect, it, vi } from "vite-plus/test";
vi.mock("@expo/ui/swift-ui", () => ({
HStack: "HStack",
Image: "Image",
Spacer: "Spacer",
Text: "Text",
VStack: "VStack",
ZStack: "ZStack",
}));
vi.mock("@expo/ui/swift-ui/modifiers", () => ({
font: (value: unknown) => value,
foregroundStyle: (value: unknown) => value,
frame: (value: unknown) => value,
layoutPriority: (value: unknown) => value,
lineLimit: (value: unknown) => value,
padding: (value: unknown) => value,
resizable: (value: unknown) => value,
widgetURL: (value: unknown) => ({ widgetURL: value }),
}));
vi.mock("expo-widgets", () => ({
createLiveActivity: vi.fn((name: string, layout: unknown) => ({ layout, name })),
}));
import {
AgentActivity,
type AgentActivityProps,
type AgentActivityRowProps,
} from "./AgentActivity";
function makeRow(overrides: Partial<AgentActivityRowProps>): AgentActivityRowProps {
return {
environmentId: "env-1",
threadId: "thread-1",
projectTitle: "Project",
threadTitle: "Thread",
modelTitle: "gpt-5.4",
phase: "running",
status: "Working",
updatedAt: "2026-05-25T13:07:00.000Z",
deepLink: "/threads/env-1/thread-1",
...overrides,
};
}
const props = {
title: "T3 Code",
subtitle: "Agent work in progress",
activeCount: 1,
updatedAt: "2026-05-25T13:07:00.000Z",
activities: [],
} satisfies AgentActivityProps;
const environment = {
colorScheme: "dark",
isLuminanceReduced: false,
} as const;
const lightEnvironment = {
colorScheme: "light",
isLuminanceReduced: false,
} as const;
describe("AgentActivity widget layout", () => {
it("tints each row by its own phase using the web sidebar's dark palette", () => {
const layout = AgentActivity(
{
...props,
activeCount: 2,
activities: [
makeRow({}),
makeRow({ threadId: "thread-2", phase: "waiting_for_approval", status: "Approval" }),
],
},
environment as never,
);
const banner = JSON.stringify(layout.banner);
expect(banner).toContain("#7dd3fc"); // sky-300: running
expect(banner).toContain("#fcd34d"); // amber-300: waiting_for_approval
});
it("switches to the web sidebar's light palette when the scheme is light", () => {
// macOS (iPhone Mirroring / Mac notification center) renders the activity
// on a light background; the dark-material palette is illegible there.
const layout = AgentActivity(
{
...props,
activeCount: 2,
activities: [
makeRow({}),
makeRow({ threadId: "thread-2", phase: "waiting_for_approval", status: "Approval" }),
],
},
lightEnvironment as never,
);
const banner = JSON.stringify(layout.banner);
expect(banner).toContain("#0284c7"); // sky-600: running
expect(banner).toContain("#d97706"); // amber-600: waiting_for_approval
expect(banner).not.toContain("#7dd3fc");
expect(banner).not.toContain("#fcd34d");
});
it("orders rows attention-first in the banner", () => {
const layout = AgentActivity(
{
...props,
activeCount: 2,
activities: [
makeRow({ threadTitle: "Working thread" }),
makeRow({
threadId: "thread-2",
threadTitle: "Blocked thread",
phase: "waiting_for_approval",
status: "Approval",
}),
],
},
environment as never,
);
const banner = JSON.stringify(layout.banner);
expect(banner.indexOf("Blocked thread")).toBeGreaterThan(-1);
expect(banner.indexOf("Blocked thread")).toBeLessThan(banner.indexOf("Working thread"));
});
it("summarizes the attention count in the banner header", () => {
const layout = AgentActivity(
{
...props,
activeCount: 3,
activities: [
makeRow({}),
makeRow({ threadId: "thread-2", phase: "waiting_for_input", status: "Input" }),
],
},
environment as never,
);
const banner = JSON.stringify(layout.banner);
expect(banner).toContain("3 active agents");
expect(banner).toContain("1 needs attention");
});
it("uses the attention tint for the compact presentations when a row needs input", () => {
const layout = AgentActivity(
{
...props,
activeCount: 2,
activities: [
makeRow({}),
makeRow({ threadId: "thread-2", phase: "waiting_for_input", status: "Input" }),
],
},
environment as never,
);
expect(JSON.stringify(layout.compactLeading)).toContain("#a5b4fc"); // indigo-300
expect(JSON.stringify(layout.compactTrailing)).toContain("Input");
expect(JSON.stringify(layout.minimal)).toContain("#a5b4fc");
});
it("deep links the banner to the row that needs attention", () => {
const layout = AgentActivity(
{
...props,
activeCount: 2,
activities: [
makeRow({}),
makeRow({
threadId: "thread-2",
phase: "waiting_for_approval",
status: "Approval",
deepLink: "/threads/env-1/thread-2",
}),
],
},
environment as never,
);
expect(JSON.stringify(layout.banner)).toContain(
'"widgetURL":"t3code://threads/env-1/thread-2"',
);
});
it("deep links the banner to the first row when nothing needs attention", () => {
const layout = AgentActivity({ ...props, activities: [makeRow({})] }, environment as never);
expect(JSON.stringify(layout.banner)).toContain(
'"widgetURL":"t3code://threads/env-1/thread-1"',
);
});
it("omits the deep link for unsafe paths and empty aggregates", () => {
expect(JSON.stringify(AgentActivity(props, environment as never))).not.toContain("widgetURL");
expect(
JSON.stringify(
AgentActivity(
{ ...props, activities: [makeRow({ deepLink: "//evil.example" })] },
environment as never,
),
),
).not.toContain("widgetURL");
});
it("leads with the outcome instead of a zero count when nothing is active", () => {
const layout = AgentActivity(
{
...props,
subtitle: "Agent work completed",
activeCount: 0,
activities: [makeRow({ phase: "completed", status: "Done" })],
},
environment as never,
);
const banner = JSON.stringify(layout.banner);
expect(banner).toContain("Agent work completed");
expect(banner).not.toContain("0 active");
expect(banner).toContain("#6ee7b7"); // emerald-300 header tint
expect(JSON.stringify(layout.compactTrailing)).toContain("Done");
expect(JSON.stringify(layout.compactTrailing)).not.toContain("0 active");
expect(JSON.stringify(layout.expandedLeading)).toContain("Done");
expect(JSON.stringify(layout.minimal)).toContain("checkmark.circle.fill");
expect(JSON.stringify(layout.bannerSmall)).toContain("Done");
});
it("reads Failed when the finished work ended in failure", () => {
const layout = AgentActivity(
{
...props,
subtitle: "Agent work failed",
activeCount: 0,
activities: [makeRow({ phase: "failed", status: "Failed" })],
},
environment as never,
);
const banner = JSON.stringify(layout.banner);
expect(banner).toContain("Agent work failed");
expect(banner).toContain("#fca5a5"); // red-300 header tint
expect(JSON.stringify(layout.compactTrailing)).toContain("Failed");
expect(JSON.stringify(layout.expandedLeading)).toContain("Failed");
expect(JSON.stringify(layout.minimal)).toContain("xmark.octagon.fill");
});
it("lets a failure dominate mixed finished outcomes across every presentation", () => {
const layout = AgentActivity(
{
...props,
// The server subtitle keys off the newest terminal row (completed
// here); the layout must still read Failed everywhere so the header
// text never disagrees with the tint, count slots, or minimal glyph.
subtitle: "Agent work completed",
activeCount: 0,
activities: [
makeRow({ phase: "completed", status: "Done" }),
makeRow({ threadId: "thread-2", phase: "failed", status: "Failed" }),
],
},
environment as never,
);
const banner = JSON.stringify(layout.banner);
expect(banner).toContain("Agent work failed");
expect(banner).not.toContain("Agent work completed");
expect(banner).toContain("#fca5a5"); // red-300 header tint
expect(JSON.stringify(layout.compactTrailing)).toContain("Failed");
expect(JSON.stringify(layout.expandedLeading)).toContain("Failed");
expect(JSON.stringify(layout.minimal)).toContain("xmark.octagon.fill");
});
it("renders up to five rows in the banner", () => {
const layout = AgentActivity(
{
...props,
activeCount: 6,
activities: [1, 2, 3, 4, 5, 6].map((n) =>
makeRow({ threadId: `t${n}`, threadTitle: `Thread ${n}` }),
),
},
environment as never,
);
const banner = JSON.stringify(layout.banner);
for (const visible of [1, 2, 3, 4, 5]) {
expect(banner).toContain(`Thread ${visible}`);
}
expect(banner).not.toContain("Thread 6");
});
});