@@ -3,8 +3,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
33import { createImportedCustomThemeFixture } from "../test-helpers/custom-theme.ts" ;
44import { createStorageMock } from "../test-helpers/storage.ts" ;
55import {
6+ loadLocalAssistantIdentity ,
67 loadLocalUserIdentity ,
78 loadSettings ,
9+ saveLocalAssistantIdentity ,
810 saveLocalUserIdentity ,
911 saveSettings ,
1012} from "./storage.ts" ;
@@ -680,3 +682,70 @@ describe("loadSettings default gateway URL derivation", () => {
680682 expect ( localStorage . getItem ( "openclaw.control.user.v1" ) ) . toBeNull ( ) ;
681683 } ) ;
682684} ) ;
685+
686+ describe ( "loadLocalAssistantIdentity / saveLocalAssistantIdentity" , ( ) => {
687+ beforeEach ( ( ) => {
688+ vi . stubGlobal ( "localStorage" , createStorageMock ( ) ) ;
689+ localStorage . clear ( ) ;
690+ } ) ;
691+
692+ afterEach ( ( ) => {
693+ vi . restoreAllMocks ( ) ;
694+ } ) ;
695+
696+ it ( "scopes avatar override to agent when agentId is provided" , ( ) => {
697+ saveLocalAssistantIdentity ( { avatar : "data:img/main.png" } , "main" ) ;
698+ saveLocalAssistantIdentity ( { avatar : "data:img/stockclaw.png" } , "stockclaw" ) ;
699+
700+ expect ( loadLocalAssistantIdentity ( "main" ) . avatar ) . toBe ( "data:img/main.png" ) ;
701+ expect ( loadLocalAssistantIdentity ( "stockclaw" ) . avatar ) . toBe ( "data:img/stockclaw.png" ) ;
702+ } ) ;
703+
704+ it ( "uses unscoped key when agentId is not provided (backward compat)" , ( ) => {
705+ saveLocalAssistantIdentity ( { avatar : "data:img/legacy.png" } ) ;
706+
707+ expect ( loadLocalAssistantIdentity ( ) . avatar ) . toBe ( "data:img/legacy.png" ) ;
708+ expect ( loadLocalAssistantIdentity ( null ) . avatar ) . toBe ( "data:img/legacy.png" ) ;
709+ expect ( localStorage . getItem ( "openclaw.control.assistant.v1" ) ) . toBe (
710+ '{"avatar":"data:img/legacy.png"}' ,
711+ ) ;
712+ } ) ;
713+
714+ it ( "migrates legacy unscoped value to scoped key on first access" , ( ) => {
715+ // Simulate a user who set an avatar before this fix
716+ localStorage . setItem (
717+ "openclaw.control.assistant.v1" ,
718+ JSON . stringify ( { avatar : "data:img/legacy.png" } ) ,
719+ ) ;
720+
721+ // First access with agentId should read legacy and migrate
722+ const result = loadLocalAssistantIdentity ( "main" ) ;
723+ expect ( result . avatar ) . toBe ( "data:img/legacy.png" ) ;
724+
725+ // Legacy key should be removed after migration
726+ expect ( localStorage . getItem ( "openclaw.control.assistant.v1" ) ) . toBeNull ( ) ;
727+
728+ // Scoped key should now have the migrated value
729+ expect ( JSON . parse ( localStorage . getItem ( "openclaw.control.assistant.v1:main" ) ?? "{}" ) ) . toEqual ( {
730+ avatar : "data:img/legacy.png" ,
731+ } ) ;
732+ } ) ;
733+
734+ it ( "clears scoped avatar when setting null" , ( ) => {
735+ saveLocalAssistantIdentity ( { avatar : "data:img/main.png" } , "main" ) ;
736+ expect ( loadLocalAssistantIdentity ( "main" ) . avatar ) . toBe ( "data:img/main.png" ) ;
737+
738+ saveLocalAssistantIdentity ( { avatar : null } , "main" ) ;
739+ expect ( loadLocalAssistantIdentity ( "main" ) . avatar ) . toBeNull ( ) ;
740+ expect ( localStorage . getItem ( "openclaw.control.assistant.v1:main" ) ) . toBeNull ( ) ;
741+ } ) ;
742+
743+ it ( "does not migrate empty legacy value" , ( ) => {
744+ localStorage . setItem ( "openclaw.control.assistant.v1" , JSON . stringify ( { avatar : null } ) ) ;
745+
746+ const result = loadLocalAssistantIdentity ( "main" ) ;
747+ expect ( result . avatar ) . toBeNull ( ) ;
748+ // Legacy key should still be there since migration only happens for non-null values
749+ expect ( localStorage . getItem ( "openclaw.control.assistant.v1" ) ) . toBe ( '{"avatar":null}' ) ;
750+ } ) ;
751+ } ) ;
0 commit comments