diff --git a/src/gateway/session-utils-creators.test.ts b/src/gateway/session-utils-creators.test.ts index bad7a1a5af0d..9f3d0aafb97a 100644 --- a/src/gateway/session-utils-creators.test.ts +++ b/src/gateway/session-utils-creators.test.ts @@ -15,10 +15,90 @@ const getUserProfileListItem = vi.hoisted(() => ); vi.mock("../state/user-profiles.js", () => ({ getUserProfileListItem })); +vi.mock("./session-utils-row.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + projectSessionActor: ( + actor: Parameters[0], + identities: Parameters[1], + ) => { + if (actor?.id === "shared-id") { + return actor.type === "human" + ? { type: actor.type, id: actor.id, label: "Alpha" } + : { type: actor.type, id: actor.id, label: "Zulu", avatarUrl: "/avatar" }; + } + if (actor?.id === "unicode-id") { + return { + type: actor.type, + id: actor.id, + label: actor.type === "human" ? "é" : "e\u0301", + }; + } + return actual.projectSessionActor(actor, identities); + }, + }; +}); import { listSessionsFromStore, listSessionsFromStoreAsync } from "./session-utils.js"; -afterEach(() => vi.restoreAllMocks()); +afterEach(() => { + vi.restoreAllMocks(); + getUserProfileListItem.mockClear(); +}); + +it("keeps creator labels and avatars stable across actor order", () => { + const actorOrders = [ + ["human", "agent"], + ["agent", "human"], + ] as const; + for (const actorOrder of actorOrders) { + const store = Object.fromEntries( + actorOrder.map((type, index) => [ + `agent:main:${index}`, + { + createdActor: { type, id: "shared-id" }, + sessionId: `session-${index}`, + updatedAt: 2 - index, + } satisfies SessionEntry, + ]), + ); + const result = listSessionsFromStore({ + cfg: {} as OpenClawConfig, + storePath: "/tmp/openclaw-session-creator-order", + store, + opts: { archived: "all" }, + }); + + expect(result.creators).toEqual([{ id: "shared-id", label: "Alpha", avatarUrl: "/avatar" }]); + } +}); + +it("breaks locale-equivalent creator label ties deterministically", () => { + for (const actorOrder of [ + ["human", "agent"], + ["agent", "human"], + ] as const) { + const store = Object.fromEntries( + actorOrder.map((type, index) => [ + `agent:main:unicode-${index}`, + { + createdActor: { type, id: "unicode-id" }, + sessionId: `unicode-session-${index}`, + updatedAt: 2 - index, + } satisfies SessionEntry, + ]), + ); + const result = listSessionsFromStore({ + cfg: {} as OpenClawConfig, + storePath: "/tmp/openclaw-session-creator-unicode-order", + store, + opts: { archived: "all" }, + }); + + expect(result.creators).toEqual([{ id: "unicode-id", label: "e\u0301" }]); + } +}); it("returns the complete deterministic creator facet independently of pagination", () => { const store: Record = { diff --git a/src/gateway/session-utils-list.ts b/src/gateway/session-utils-list.ts index ccaaf4a8b83b..f3f0e746107f 100644 --- a/src/gateway/session-utils-list.ts +++ b/src/gateway/session-utils-list.ts @@ -82,6 +82,16 @@ type SessionEntrySelection = { hasMore: boolean; }; +function preferredCreatorIdentityValue( + current: string | undefined, + candidate: string | undefined, +): string | undefined { + if (!current || !candidate) { + return current ?? candidate; + } + return candidate < current ? candidate : current; +} + function addSessionCreatorIdentity( creators: Map, entry: SessionEntry, @@ -95,15 +105,13 @@ function addSessionCreatorIdentity( const label = normalizeOptionalString(actor?.label); const avatarUrl = normalizeOptionalString(actor?.avatarUrl); const existing = creators.get(id); - if ( - !existing || - (label && (!existing.label || label.localeCompare(existing.label) < 0)) || - (avatarUrl && !existing.avatarUrl) - ) { + const preferredLabel = preferredCreatorIdentityValue(existing?.label, label); + const preferredAvatarUrl = preferredCreatorIdentityValue(existing?.avatarUrl, avatarUrl); + if (!existing || preferredLabel !== existing.label || preferredAvatarUrl !== existing.avatarUrl) { creators.set(id, { id, - ...(label ? { label } : existing?.label ? { label: existing.label } : {}), - ...(avatarUrl ? { avatarUrl } : existing?.avatarUrl ? { avatarUrl: existing.avatarUrl } : {}), + ...(preferredLabel ? { label: preferredLabel } : {}), + ...(preferredAvatarUrl ? { avatarUrl: preferredAvatarUrl } : {}), }); } }