@@ -21,8 +21,19 @@ const {
2121 const removeMock = vi . fn ( ( ) => Promise . resolve ( ) ) ;
2222 return {
2323 appDbMock : {
24- getItem : vi . fn ( ) ,
25- clear : vi . fn ( ( ) => Promise . resolve ( ) ) ,
24+ getItem : vi . fn ( async ( key : string ) => {
25+ void key ;
26+ return undefined as unknown ;
27+ } ) ,
28+ setItem : vi . fn ( async ( key : string , value : unknown ) => {
29+ void key ;
30+ void value ;
31+ } ) ,
32+ removeItem : vi . fn ( async ( key : string ) => {
33+ void key ;
34+ } ) ,
35+ keys : vi . fn ( async ( ) : Promise < string [ ] > => [ ] ) ,
36+ clear : vi . fn ( async ( ) => undefined ) ,
2637 } ,
2738 dirMock : vi . fn ( ( ) => ( {
2839 exists : vi . fn ( ( ) => Promise . resolve ( true ) ) ,
@@ -60,9 +71,21 @@ vi.mock("../store/storage/storageConfig", () => ({
6071
6172vi . mock ( "../lib/byok/client" , ( ) => ( {
6273 encryptSecret : encryptSecretMock ,
74+ fetchWithByokRetry : vi . fn ( ( requestFactory ) => requestFactory ( ) ) ,
6375} ) ) ;
6476
65- const { clearBrowserAppData } = await import ( "../lib/data/clearAppData" ) ;
77+ vi . mock ( "../lib/api/client" , async ( ) => {
78+ const actual = await vi . importActual ( "../lib/api/client" ) ;
79+ return {
80+ ...actual ,
81+ signedApiFetch : vi . fn ( ( input : RequestInfo | URL , init ?: RequestInit ) =>
82+ fetch ( input , init ) ,
83+ ) ,
84+ } ;
85+ } ) ;
86+
87+ const { clearBrowserAppData, clearBrowserAppDataSources } =
88+ await import ( "../lib/data/clearAppData" ) ;
6689const { deleteOPFSDirectory } = await import ( "../utils/opfs" ) ;
6790
6891const ragConfig : RAGConfig = {
@@ -159,6 +182,93 @@ describe("clear app data", () => {
159182 expect ( appDbMock . clear ) . toHaveBeenCalled ( ) ;
160183 } ) ;
161184
185+ it ( "clears cache metadata without clearing user settings or stores" , async ( ) => {
186+ appDbMock . getItem . mockResolvedValueOnce (
187+ JSON . stringify ( {
188+ state : {
189+ marketPlugins : [ { id : "cached-plugin" } ] ,
190+ marketPluginsTimestamp : 123 ,
191+ marketAgents : [ { identifier : "cached-agent" } ] ,
192+ marketAgentsTimestamp : 456 ,
193+ marketAgentsLocale : "zh" ,
194+ skillCatalogs : { en : { skills : [ ] } } ,
195+ skillCatalogTimestamps : { en : 789 } ,
196+ skillDefinitions : { "en:test.json" : { id : "test" } } ,
197+ skillDefinitionTimestamps : { "en:test.json" : 999 } ,
198+ modelMetadata : { "gpt-test" : { id : "gpt-test" } } ,
199+ modelMetadataTimestamp : 321 ,
200+ installedPlugins : [ { id : "keep-plugin" } ] ,
201+ } ,
202+ version : 4 ,
203+ } ) ,
204+ ) ;
205+
206+ await clearBrowserAppDataSources ( {
207+ sources : [ "cache" ] ,
208+ rag : ragConfig ,
209+ } ) ;
210+
211+ expect ( appDbMock . clear ) . not . toHaveBeenCalled ( ) ;
212+ expect ( localforageClearMock ) . not . toHaveBeenCalled ( ) ;
213+ expect ( appDbMock . removeItem ) . not . toHaveBeenCalled ( ) ;
214+ expect ( appDbMock . setItem ) . toHaveBeenCalledWith (
215+ "neo-chat-settings" ,
216+ expect . stringContaining ( '"installedPlugins":[{"id":"keep-plugin"}]' ) ,
217+ ) ;
218+ const saved = JSON . parse ( appDbMock . setItem . mock . calls [ 0 ] [ 1 ] as string ) ;
219+ expect ( saved . state ) . toMatchObject ( {
220+ marketPlugins : [ ] ,
221+ marketPluginsTimestamp : 0 ,
222+ marketAgents : [ ] ,
223+ marketAgentsTimestamp : 0 ,
224+ marketAgentsLocale : "" ,
225+ skillCatalogs : { } ,
226+ skillCatalogTimestamps : { } ,
227+ skillDefinitions : { } ,
228+ skillDefinitionTimestamps : { } ,
229+ modelMetadata : { } ,
230+ modelMetadataTimestamp : 0 ,
231+ } ) ;
232+ } ) ;
233+
234+ it ( "clears chat metadata and per-session message records when requested" , async ( ) => {
235+ appDbMock . keys . mockResolvedValueOnce ( [
236+ "neo-chat-storage" ,
237+ "session_messages_a" ,
238+ "session_messages_b" ,
239+ "unrelated" ,
240+ ] ) ;
241+
242+ await clearBrowserAppDataSources ( {
243+ sources : [ "chats" ] ,
244+ rag : ragConfig ,
245+ } ) ;
246+
247+ expect ( appDbMock . removeItem ) . toHaveBeenCalledWith ( "neo-chat-storage" ) ;
248+ expect ( appDbMock . removeItem ) . toHaveBeenCalledWith ( "session_messages_a" ) ;
249+ expect ( appDbMock . removeItem ) . toHaveBeenCalledWith ( "session_messages_b" ) ;
250+ expect ( appDbMock . removeItem ) . not . toHaveBeenCalledWith ( "unrelated" ) ;
251+ expect ( appDbMock . clear ) . not . toHaveBeenCalled ( ) ;
252+ } ) ;
253+
254+ it ( "clears knowledge metadata, vectors, and OPFS knowledge files when requested" , async ( ) => {
255+ await clearBrowserAppDataSources ( {
256+ sources : [ "knowledge" ] ,
257+ rag : ragConfig ,
258+ } ) ;
259+
260+ expect ( fetch ) . toHaveBeenCalledWith (
261+ "/api/rag/delete" ,
262+ expect . objectContaining ( {
263+ method : "POST" ,
264+ body : expect . stringContaining ( '"namespace":"collection-1"' ) ,
265+ } ) ,
266+ ) ;
267+ expect ( dirMock ) . toHaveBeenCalledWith ( "knowledge-base" ) ;
268+ expect ( appDbMock . removeItem ) . toHaveBeenCalledWith ( "knowledge-storage" ) ;
269+ expect ( appDbMock . clear ) . not . toHaveBeenCalled ( ) ;
270+ } ) ;
271+
162272 it ( "rejects unsafe OPFS directory paths" , async ( ) => {
163273 await deleteOPFSDirectory ( "../secret" ) ;
164274
0 commit comments