Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
272 lines
7.7 KiB
TypeScript
272 lines
7.7 KiB
TypeScript
import { expect, test } from "vitest";
|
|
import {
|
|
createAgentGUIConversationActivityActivation,
|
|
localDayStartUnixMs,
|
|
projectAgentGUIConversationActivity,
|
|
reconcileAgentGUIConversationActivityActivation
|
|
} from "./agentGuiConversationActivityView.ts";
|
|
import type { AgentGUIConversationSummary } from "./agentGuiConversationTypes.ts";
|
|
|
|
const NOW = new Date(2026, 7, 6, 14, 30).getTime();
|
|
const HOUR_MS = 60 * 60 * 1000;
|
|
const DAY_MS = 24 * HOUR_MS;
|
|
|
|
test("activity activation ranks live priority and snapshots recent date buckets", () => {
|
|
const activation = createAgentGUIConversationActivityActivation(
|
|
[
|
|
conversation("active", { status: "working", time: NOW - HOUR_MS }),
|
|
conversation("recent", { time: NOW - 2 * HOUR_MS }),
|
|
conversation("unread", {
|
|
hasUnreadCompletion: true,
|
|
time: NOW - 3 * HOUR_MS
|
|
}),
|
|
conversation("waiting", {
|
|
needsUserAction: true,
|
|
time: NOW - 4 * HOUR_MS
|
|
}),
|
|
conversation("old", { time: NOW - 8 * DAY_MS })
|
|
],
|
|
NOW
|
|
);
|
|
|
|
expect(projectAgentGUIConversationActivity(activation)).toEqual({
|
|
priorityIds: ["waiting", "unread", "active"],
|
|
priorityReasonsById: new Map([
|
|
["waiting", "waiting"],
|
|
["unread", "unread"],
|
|
["active", "active"]
|
|
]),
|
|
referenceDayStartUnixMs: localDayStartUnixMs(NOW),
|
|
recentSections: [
|
|
{
|
|
dayStartUnixMs: localDayStartUnixMs(NOW),
|
|
ids: ["recent"]
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
test("activity reconciliation retains members and frozen recency after live facts change", () => {
|
|
const initial = createAgentGUIConversationActivityActivation(
|
|
[
|
|
conversation("waiting", { needsUserAction: true, time: NOW - HOUR_MS }),
|
|
conversation("recent", { time: NOW - 2 * HOUR_MS })
|
|
],
|
|
NOW
|
|
);
|
|
const reconciled = reconcileAgentGUIConversationActivityActivation(initial, [
|
|
conversation("waiting", { time: NOW + HOUR_MS }),
|
|
conversation("recent", { time: NOW + 2 * DAY_MS })
|
|
]);
|
|
|
|
expect(projectAgentGUIConversationActivity(reconciled)).toEqual({
|
|
priorityIds: ["waiting"],
|
|
priorityReasonsById: new Map([["waiting", "waiting"]]),
|
|
referenceDayStartUnixMs: localDayStartUnixMs(NOW),
|
|
recentSections: [
|
|
{
|
|
dayStartUnixMs: localDayStartUnixMs(NOW),
|
|
ids: ["recent"]
|
|
}
|
|
]
|
|
});
|
|
});
|
|
|
|
test("activity reconciliation keeps existing Priority members through a list refresh", () => {
|
|
const initial = createAgentGUIConversationActivityActivation(
|
|
[
|
|
conversation("waiting", {
|
|
needsUserAction: true,
|
|
time: NOW - HOUR_MS
|
|
}),
|
|
conversation("unread", {
|
|
hasUnreadCompletion: true,
|
|
time: NOW - 2 * HOUR_MS
|
|
})
|
|
],
|
|
NOW
|
|
);
|
|
const refreshed = reconcileAgentGUIConversationActivityActivation(initial, [
|
|
conversation("new-active", {
|
|
status: "working",
|
|
time: NOW + HOUR_MS
|
|
})
|
|
]);
|
|
|
|
expect(projectAgentGUIConversationActivity(refreshed).priorityIds).toEqual([
|
|
"waiting",
|
|
"unread",
|
|
"new-active"
|
|
]);
|
|
expect(refreshed.priority.slice(0, 2)).toEqual(initial.priority);
|
|
});
|
|
|
|
test("activity reconciliation does not move existing members when they become read", () => {
|
|
const initial = createAgentGUIConversationActivityActivation(
|
|
[
|
|
conversation("waiting", {
|
|
needsUserAction: true,
|
|
time: NOW - HOUR_MS
|
|
}),
|
|
conversation("unread", {
|
|
hasUnreadCompletion: true,
|
|
time: NOW - 2 * HOUR_MS
|
|
})
|
|
],
|
|
NOW
|
|
);
|
|
const read = reconcileAgentGUIConversationActivityActivation(initial, [
|
|
conversation("unread", { time: NOW + 2 * DAY_MS }),
|
|
conversation("waiting", { time: NOW + DAY_MS })
|
|
]);
|
|
|
|
expect(projectAgentGUIConversationActivity(read).priorityIds).toEqual([
|
|
"waiting",
|
|
"unread"
|
|
]);
|
|
expect(read.priority.map((member) => member.priorityReason)).toEqual([
|
|
"waiting",
|
|
"unread"
|
|
]);
|
|
});
|
|
|
|
test("activity reconciliation admits only live late sessions and retains Priority members", () => {
|
|
const initial = createAgentGUIConversationActivityActivation(
|
|
[
|
|
conversation("keep", { status: "working", time: NOW - HOUR_MS }),
|
|
conversation("promote", { time: NOW - 2 * HOUR_MS })
|
|
],
|
|
NOW
|
|
);
|
|
const reconciled = reconcileAgentGUIConversationActivityActivation(initial, [
|
|
conversation("promote", {
|
|
hasUnreadCompletion: true,
|
|
time: NOW + HOUR_MS
|
|
}),
|
|
conversation("pushed-active", {
|
|
status: "working",
|
|
time: NOW + 2 * HOUR_MS
|
|
}),
|
|
conversation("pushed-idle", { time: NOW + 3 * HOUR_MS })
|
|
]);
|
|
|
|
expect(projectAgentGUIConversationActivity(reconciled)).toEqual({
|
|
priorityIds: ["promote", "pushed-active", "keep"],
|
|
priorityReasonsById: new Map([
|
|
["promote", "unread"],
|
|
["pushed-active", "active"],
|
|
["keep", "active"]
|
|
]),
|
|
recentSections: [],
|
|
referenceDayStartUnixMs: localDayStartUnixMs(NOW)
|
|
});
|
|
expect(new Set(reconciled.priority.map((member) => member.id)).size).toBe(
|
|
reconciled.priority.length
|
|
);
|
|
});
|
|
|
|
test("activity reconciliation removes a deleted Priority member immediately", () => {
|
|
const initial = createAgentGUIConversationActivityActivation(
|
|
[conversation("deleted", { status: "working", time: NOW })],
|
|
NOW
|
|
);
|
|
const reconciled = reconcileAgentGUIConversationActivityActivation(
|
|
initial,
|
|
[],
|
|
{ deleted: true }
|
|
);
|
|
|
|
expect(projectAgentGUIConversationActivity(reconciled).priorityIds).toEqual(
|
|
[]
|
|
);
|
|
});
|
|
|
|
test("activity reconciliation preserves activation identity when nothing changes", () => {
|
|
const conversations = [conversation("recent", { time: NOW - HOUR_MS })];
|
|
const initial = createAgentGUIConversationActivityActivation(
|
|
conversations,
|
|
NOW
|
|
);
|
|
expect(
|
|
reconcileAgentGUIConversationActivityActivation(initial, conversations)
|
|
).toBe(initial);
|
|
});
|
|
|
|
test("activity activation preserves source order for equal rank and recency", () => {
|
|
const activation = createAgentGUIConversationActivityActivation(
|
|
[
|
|
conversation("second-by-id", { status: "working", time: NOW }),
|
|
conversation("first-by-id", { status: "working", time: NOW })
|
|
],
|
|
NOW
|
|
);
|
|
|
|
expect(projectAgentGUIConversationActivity(activation).priorityIds).toEqual([
|
|
"second-by-id",
|
|
"first-by-id"
|
|
]);
|
|
});
|
|
|
|
test("activity activation uses seven local calendar days at the cutoff", () => {
|
|
const openedAt = new Date(2026, 7, 6, 14, 30).getTime();
|
|
const activation = createAgentGUIConversationActivityActivation(
|
|
[
|
|
conversation("cutoff-day", {
|
|
time: new Date(2026, 6, 31, 0, 0).getTime()
|
|
}),
|
|
conversation("before-cutoff", {
|
|
time: new Date(2026, 6, 30, 23, 59).getTime()
|
|
})
|
|
],
|
|
openedAt
|
|
);
|
|
|
|
expect(
|
|
projectAgentGUIConversationActivity(activation).recentSections
|
|
).toEqual([
|
|
{
|
|
dayStartUnixMs: new Date(2026, 6, 31, 0, 0).getTime(),
|
|
ids: ["cutoff-day"]
|
|
}
|
|
]);
|
|
});
|
|
|
|
test("activity reconciliation ignores upstream collection reordering", () => {
|
|
const conversations = [
|
|
conversation("newer", { time: NOW - HOUR_MS }),
|
|
conversation("older", { time: NOW - 2 * HOUR_MS })
|
|
];
|
|
const initial = createAgentGUIConversationActivityActivation(
|
|
conversations,
|
|
NOW
|
|
);
|
|
|
|
expect(
|
|
reconcileAgentGUIConversationActivityActivation(
|
|
initial,
|
|
[...conversations].reverse()
|
|
)
|
|
).toBe(initial);
|
|
});
|
|
|
|
function conversation(
|
|
id: string,
|
|
overrides: {
|
|
hasUnreadCompletion?: boolean;
|
|
needsUserAction?: boolean;
|
|
status?: AgentGUIConversationSummary["status"];
|
|
time: number;
|
|
}
|
|
): AgentGUIConversationSummary {
|
|
return {
|
|
cwd: "/workspace",
|
|
hasUnreadCompletion: overrides.hasUnreadCompletion,
|
|
id,
|
|
needsUserAction: overrides.needsUserAction,
|
|
provider: "codex",
|
|
status: overrides.status ?? "ready",
|
|
title: id,
|
|
updatedAtUnixMs: overrides.time
|
|
};
|
|
}
|