11// Control UI tests cover agents utils behavior.
2- import { describe , expect , it } from "vitest" ;
2+ import { describe , expect , it , vi } from "vitest" ;
33import { AVATAR_MAX_DATA_URL_CHARS } from "../../../../src/shared/avatar-limits.js" ;
44import {
55 assistantAvatarFallbackUrl ,
@@ -8,7 +8,12 @@ import {
88 resolveAssistantTextAvatar ,
99 resolveChatAvatarRenderUrl ,
1010} from "../avatar.ts" ;
11- import { buildAgentContext , formatBytes , resolveEffectiveModelFallbacks } from "./display.ts" ;
11+ import {
12+ buildAgentContext ,
13+ formatBytes ,
14+ resolveEffectiveModelFallbacks ,
15+ resolveFallbackAvatarInitial ,
16+ } from "./display.ts" ;
1217
1318describe ( "formatBytes" , ( ) => {
1419 it ( "preserves the Control UI byte-size display contract" , ( ) => {
@@ -67,6 +72,73 @@ describe("assistantAvatarFallbackUrl", () => {
6772 } ) ;
6873} ) ;
6974
75+ describe ( "resolveFallbackAvatarInitial" , ( ) => {
76+ it ( "keeps flag and ZWJ emoji clusters intact" , ( ) => {
77+ expect ( resolveFallbackAvatarInitial ( "🇺🇸Team" ) ) . toBe ( "🇺🇸" ) ;
78+ expect ( resolveFallbackAvatarInitial ( "👨💻Dev" ) ) . toBe ( "👨💻" ) ;
79+ expect ( resolveFallbackAvatarInitial ( "alpha" ) ) . toBe ( "A" ) ;
80+ expect ( resolveFallbackAvatarInitial ( "東京" ) ) . toBe ( "東" ) ;
81+ expect ( resolveFallbackAvatarInitial ( "Émile" ) ) . toBe ( "É" ) ;
82+ expect ( resolveFallbackAvatarInitial ( " " ) ) . toBe ( "?" ) ;
83+ console . log (
84+ "avatar-initial proof:" ,
85+ JSON . stringify ( {
86+ flag : resolveFallbackAvatarInitial ( "🇺🇸Team" ) ,
87+ zwj : resolveFallbackAvatarInitial ( "👨💻Dev" ) ,
88+ ascii : resolveFallbackAvatarInitial ( "alpha" ) ,
89+ tokyo : resolveFallbackAvatarInitial ( "東京" ) ,
90+ accented : resolveFallbackAvatarInitial ( "Émile" ) ,
91+ } ) ,
92+ ) ;
93+ } ) ;
94+
95+ it ( "keeps locale-independent ASCII casing (Turkish i)" , ( ) => {
96+ const previous = process . env . LC_ALL ;
97+ try {
98+ // Exercise the same toUpperCase path the Control UI fallback uses.
99+ expect ( resolveFallbackAvatarInitial ( "istanbul" ) ) . toBe ( "I" ) ;
100+ } finally {
101+ if ( previous === undefined ) {
102+ delete process . env . LC_ALL ;
103+ } else {
104+ process . env . LC_ALL = previous ;
105+ }
106+ }
107+ } ) ;
108+
109+ it ( "preserves safe BMP letters/digits without Segmenter; refuses flag/ZWJ splits" , async ( ) => {
110+ const originalSegmenter = Intl . Segmenter ;
111+ Object . defineProperty ( Intl , "Segmenter" , { configurable : true , value : undefined } ) ;
112+ vi . resetModules ( ) ;
113+ try {
114+ const { resolveFallbackAvatarInitial : resolveWithoutSegmenter } =
115+ await import ( "./display.ts" ) ;
116+ expect ( resolveWithoutSegmenter ( "alpha" ) ) . toBe ( "A" ) ;
117+ expect ( resolveWithoutSegmenter ( "東京" ) ) . toBe ( "東" ) ;
118+ expect ( resolveWithoutSegmenter ( "Émile" ) ) . toBe ( "É" ) ;
119+ expect ( resolveWithoutSegmenter ( "9lives" ) ) . toBe ( "9" ) ;
120+ expect ( resolveWithoutSegmenter ( "🇺🇸" ) ) . toBe ( "?" ) ;
121+ expect ( resolveWithoutSegmenter ( "🇺🇸Team" ) ) . toBe ( "?" ) ;
122+ expect ( resolveWithoutSegmenter ( "👨💻" ) ) . toBe ( "?" ) ;
123+ expect ( resolveWithoutSegmenter ( "👨💻Dev" ) ) . toBe ( "?" ) ;
124+ console . log (
125+ "avatar-initial Segmenter-fallback proof:" ,
126+ JSON . stringify ( {
127+ ascii : resolveWithoutSegmenter ( "alpha" ) ,
128+ tokyo : resolveWithoutSegmenter ( "東京" ) ,
129+ accented : resolveWithoutSegmenter ( "Émile" ) ,
130+ digit : resolveWithoutSegmenter ( "9lives" ) ,
131+ flag : resolveWithoutSegmenter ( "🇺🇸" ) ,
132+ zwj : resolveWithoutSegmenter ( "👨💻" ) ,
133+ } ) ,
134+ ) ;
135+ } finally {
136+ Object . defineProperty ( Intl , "Segmenter" , { configurable : true , value : originalSegmenter } ) ;
137+ vi . resetModules ( ) ;
138+ }
139+ } ) ;
140+ } ) ;
141+
70142describe ( "resolveAssistantTextAvatar" , ( ) => {
71143 it ( "rejects unsafe invisible controls in assistant text avatars" , ( ) => {
72144 expect ( resolveAssistantTextAvatar ( "VC" ) ) . toBe ( "VC" ) ;
0 commit comments