From 38713ee5c9bd6e727d246003f98a96711706c479 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 6 Feb 2025 11:08:34 +0100 Subject: [PATCH 1/2] Make wpcom.req default response type `unknown` --- .../tests/content-tab-assistant.test.tsx | 22 ++---- src/custom-package-definitions.d.ts | 18 ++--- src/hooks/tests/use-prompt-usage.test.tsx | 18 +++-- src/hooks/use-feature-flags.tsx | 19 ++++- src/hooks/use-prompt-usage.tsx | 51 +++++++------ src/lib/oauth.ts | 11 ++- src/stores/chat-slice.ts | 74 ++++++++++++++----- src/stores/tests/chat-slice.test.ts | 28 ++++--- 8 files changed, 152 insertions(+), 89 deletions(-) diff --git a/src/components/tests/content-tab-assistant.test.tsx b/src/components/tests/content-tab-assistant.test.tsx index ff9bff006..a95614173 100644 --- a/src/components/tests/content-tab-assistant.test.tsx +++ b/src/components/tests/content-tab-assistant.test.tsx @@ -52,8 +52,8 @@ const runningSite = { }; const initialMessages = [ - generateMessage( 'Initial message 1', 'user', 0, 'chat-id', 10 ), - generateMessage( 'Initial message 2', 'assistant', 1, 'chat-id', 11 ), + generateMessage( 'Initial message 1', 'user', 0, 100, 10 ), + generateMessage( 'Initial message 2', 'assistant', 1, 100, 11 ), ]; function ContextWrapper( props: Parameters< typeof ContentTabAssistant >[ 0 ] ) { @@ -71,10 +71,8 @@ describe( 'ContentTabAssistant', () => { callback( null, { - id: 'chatcmpl-9USNsuhHWYsPAUNiOhOG2970Hjwwb', - object: 'chat.completion', - created: 1717045976, - model: 'test', + id: 100, + created_at: '2025-01-24 09:11:50', choices: [ { index: 0, @@ -84,12 +82,8 @@ describe( 'ContentTabAssistant', () => { content: 'Hello! How can I assist you today? Are you working on a WordPress project, or do you need help with something specific related to WordPress or WP-CLI?', }, - logprobs: null, - finish_reason: 'stop', }, ], - usage: { prompt_tokens: 980, completion_tokens: 36, total_tokens: 1016 }, - system_fingerprint: 'fp_777', }, { 'x-quota-max': '100', @@ -355,9 +349,9 @@ describe( 'ContentTabAssistant', () => { jest.useFakeTimers(); jest.setSystemTime( MOCKED_CURRENT_TIME ); - const messageOne = generateMessage( 'Initial message 1', 'user', 0, 'hej', 10 ); + const messageOne = generateMessage( 'Initial message 1', 'user', 0, 100, 10 ); messageOne.createdAt = MOCKED_CURRENT_TIME; - const messageTwo = generateMessage( 'Initial message 2', 'assistant', 1, 'hej', 11 ); + const messageTwo = generateMessage( 'Initial message 2', 'assistant', 1, 100, 11 ); messageTwo.createdAt = OLD_MESSAGE_TIME; store.dispatch( chatActions.setMessages( { @@ -398,9 +392,9 @@ describe( 'ContentTabAssistant', () => { } ); it( 'renders notices by importance', async () => { - const messageOne = generateMessage( 'Initial message 1', 'user', 0, 'chat-id', 10 ); + const messageOne = generateMessage( 'Initial message 1', 'user', 0, 100, 10 ); messageOne.createdAt = 0; - const messageTwo = generateMessage( 'Initial message 2', 'assistant', 1, 'chat-id', 11 ); + const messageTwo = generateMessage( 'Initial message 2', 'assistant', 1, 100, 11 ); messageTwo.createdAt = 0; store.dispatch( chatActions.setMessages( { diff --git a/src/custom-package-definitions.d.ts b/src/custom-package-definitions.d.ts index 6b89a081d..5b5bef930 100644 --- a/src/custom-package-definitions.d.ts +++ b/src/custom-package-definitions.d.ts @@ -46,38 +46,38 @@ declare module '@timfish/forge-externals-plugin' { declare module 'wpcom' { class Request { /* eslint-disable @typescript-eslint/no-explicit-any */ - get< TResponse = any >( params: object | string, query?: object ): Promise< TResponse >; - get< TResponse = any >( + get< TResponse = unknown >( params: object | string, query?: object ): Promise< TResponse >; + get< TResponse = unknown >( params: object | string, callback?: ( error: Error, data: TResponse, headers: Record< string, string > ) => void ); - get< TResponse = any >( + get< TResponse = unknown >( params: object | string, query?: object, callback?: ( error: Error, data: TResponse, headers: Record< string, string > ) => void ); - post< TResponse = any >( + post< TResponse = unknown >( params: object | string, callback?: ( error: Error, data: TResponse, headers: Record< string, string > ) => void ); - post< TResponse = any >( + post< TResponse = unknown >( params: object | string, query?: object, body?: object ): Promise< TResponse >; - post< TResponse = any >( + post< TResponse = unknown >( params: object | string, query?: object, body?: object, callback?: ( error: Error, data: TResponse, headers: Record< string, string > ) => void ); - del< TResponse = any >( params: object | string, query?: object ): Promise< TResponse >; - del< TResponse = any >( + del< TResponse = unknown >( params: object | string, query?: object ): Promise< TResponse >; + del< TResponse = unknown >( params: object | string, query?: object, callback?: ( error: Error, data: TResponse, headers: Record< string, string > ) => void ); - del< TResponse = any >( + del< TResponse = unknown >( params: object | string, callback?: ( error: Error, data: TResponse, headers: Record< string, string > ) => void ); diff --git a/src/hooks/tests/use-prompt-usage.test.tsx b/src/hooks/tests/use-prompt-usage.test.tsx index 566a32b91..d66cffc1e 100644 --- a/src/hooks/tests/use-prompt-usage.test.tsx +++ b/src/hooks/tests/use-prompt-usage.test.tsx @@ -1,8 +1,8 @@ -import { renderHook, act, waitFor } from '@testing-library/react'; +import { renderHook, waitFor } from '@testing-library/react'; import { Provider } from 'react-redux'; import { useAuth } from 'src/hooks/use-auth'; import { usePromptUsage, PromptUsageProvider } from 'src/hooks/use-prompt-usage'; -import { store } from 'src/stores'; +import { store, useRootSelector } from 'src/stores'; import type { ReactNode } from 'react'; jest.mock( 'src/hooks/use-auth', () => ( { @@ -13,6 +13,11 @@ jest.mock( 'src/hooks/use-feature-flags', () => ( { useFeatureFlags: jest.fn(), } ) ); +jest.mock( 'src/stores', () => ( { + ...jest.requireActual( 'src/stores' ), + useRootSelector: jest.fn().mockReturnValue( {} ), +} ) ); + function TestWrapper( { children }: { children: ReactNode } ) { return ( @@ -71,12 +76,13 @@ describe( 'usePromptUsage hook', () => { } ); it( 'should update prompt usage', async () => { - const { result } = renderHook( () => usePromptUsage(), { - wrapper: TestWrapper, + jest.mocked( useRootSelector ).mockReturnValueOnce( { + maxQuota: '300', + remainingQuota: '50', } ); - act( () => { - result.current.updatePromptUsage( { maxQuota: '300', remainingQuota: '50' } ); + const { result } = renderHook( () => usePromptUsage(), { + wrapper: TestWrapper, } ); expect( result.current.promptLimit ).toBe( 300 ); diff --git a/src/hooks/use-feature-flags.tsx b/src/hooks/use-feature-flags.tsx index 1717c2d1d..7e92ed0a3 100644 --- a/src/hooks/use-feature-flags.tsx +++ b/src/hooks/use-feature-flags.tsx @@ -1,7 +1,18 @@ +import * as Sentry from '@sentry/react'; import React, { createContext, useContext, ReactNode, useState, useEffect } from 'react'; +import { z } from 'zod'; import { useAuth } from 'src/hooks/use-auth'; import { getAppGlobals } from 'src/lib/app-globals'; +// In PHP, empty associative arrays are encoded as regular arrays when converted to JSON. +// This means an empty feature flags response comes as [] instead of {}. +const featureFlagsSchema = z + .object( { + terminal_wp_cli_enabled: z.boolean().optional(), + quick_deploys_enabled: z.boolean().optional(), + } ) + .catch( ( _ ) => ( {} ) ); + export interface FeatureFlagsContextType { terminalWpCliEnabled: boolean; quickDeploysEnabled: boolean; @@ -32,20 +43,22 @@ export const FeatureFlagsProvider: React.FC< FeatureFlagsProviderProps > = ( { c return; } try { - const flags = await client.req.get( { + const response = await client.req.get( { path: '/studio-app/feature-flags', apiNamespace: 'wpcom/v2', } ); + const flags = featureFlagsSchema.parse( response ); if ( cancel ) { return; } setFeatureFlags( { terminalWpCliEnabled: - Boolean( flags?.[ 'terminal_wp_cli_enabled' ] ) || terminalWpCliEnabledFromGlobals, + Boolean( flags.terminal_wp_cli_enabled ) || terminalWpCliEnabledFromGlobals, quickDeploysEnabled: - Boolean( flags?.[ 'quick_deploys_enabled' ] ) || quickDeploysEnabledFromGlobals, + Boolean( flags.quick_deploys_enabled ) || quickDeploysEnabledFromGlobals, } ); } catch ( error ) { + Sentry.captureException( error ); console.error( error ); } } diff --git a/src/hooks/use-prompt-usage.tsx b/src/hooks/use-prompt-usage.tsx index b312dfb6e..220bdbf09 100644 --- a/src/hooks/use-prompt-usage.tsx +++ b/src/hooks/use-prompt-usage.tsx @@ -1,14 +1,20 @@ import * as Sentry from '@sentry/electron/renderer'; import { useState, useEffect, useCallback, useMemo, createContext, useContext } from 'react'; +import { z } from 'zod'; import { LIMIT_OF_PROMPTS_PER_USER } from 'src/constants'; import { useAuth } from 'src/hooks/use-auth'; import { useRootSelector } from 'src/stores'; +const promptUsageSchema = z.object( { + max_quota: z.number(), + remaining_quota: z.number(), + quota_reset_date: z.string(), +} ); + type PromptUsage = { promptLimit: number; promptCount: number; fetchPromptUsage: () => Promise< void >; - updatePromptUsage: ( data: { maxQuota: string; remainingQuota: string } ) => void; userCanSendMessage: boolean; daysUntilReset: number; }; @@ -17,7 +23,6 @@ const initState: PromptUsage = { promptLimit: LIMIT_OF_PROMPTS_PER_USER, promptCount: 0, fetchPromptUsage: async () => undefined, - updatePromptUsage: ( _data: { maxQuota: string; remainingQuota: string } ) => undefined, userCanSendMessage: true, daysUntilReset: 0, }; @@ -43,21 +48,24 @@ const calculateDaysRemaining = ( quotaResetDate: string ): number => { export function PromptUsageProvider( { children }: PromptUsageProps ) { const { Provider } = promptUsageContext; - const promptUsageDict = useRootSelector( ( state ) => state.chat.promptUsageDict ); + const promptUsage = useRootSelector( ( state ) => state.chat.promptUsage ); const [ promptLimit, setPromptLimit ] = useState( LIMIT_OF_PROMPTS_PER_USER ); const [ promptCount, setPromptCount ] = useState( 0 ); const [ quotaResetDate, setQuotaResetDate ] = useState( '' ); const { client } = useAuth(); - const updatePromptUsage = useCallback( ( data: { maxQuota: string; remainingQuota: string } ) => { - const limit = parseInt( data.maxQuota as string ); - const remaining = parseInt( data.remainingQuota as string ); - if ( isNaN( limit ) || isNaN( remaining ) ) { - return; - } - setPromptLimit( limit ); - setPromptCount( limit - remaining ); - }, [] ); + const updatePromptUsage = useCallback( + ( maxQuota: string | number, remainingQuota: string | number ) => { + const limit = parseInt( maxQuota as string ); + const remaining = parseInt( remainingQuota as string ); + if ( isNaN( limit ) || isNaN( remaining ) ) { + return; + } + setPromptLimit( limit ); + setPromptCount( limit - remaining ); + }, + [] + ); const fetchPromptUsage = useCallback( async () => { if ( ! client?.req ) { @@ -68,11 +76,9 @@ export function PromptUsageProvider( { children }: PromptUsageProps ) { path: '/studio-app/ai-assistant/quota', apiNamespace: 'wpcom/v2', } ); - updatePromptUsage( { - maxQuota: response.max_quota ?? '', - remainingQuota: response.remaining_quota ?? '', - } ); - setQuotaResetDate( response.quota_reset_date || '' ); + const data = promptUsageSchema.parse( response ); + updatePromptUsage( data.max_quota, data.remaining_quota ); + setQuotaResetDate( data.quota_reset_date ); } catch ( error ) { Sentry.captureException( error ); console.error( error ); @@ -87,12 +93,10 @@ export function PromptUsageProvider( { children }: PromptUsageProps ) { }, [ fetchPromptUsage, client ] ); useEffect( () => { - if ( promptUsageDict ) { - for ( const siteId in promptUsageDict ) { - updatePromptUsage( promptUsageDict[ siteId ] ); - } + if ( promptUsage.maxQuota && promptUsage.remainingQuota ) { + updatePromptUsage( promptUsage.maxQuota, promptUsage.remainingQuota ); } - }, [ promptUsageDict, updatePromptUsage ] ); + }, [ promptUsage, updatePromptUsage ] ); const daysUntilReset = useMemo( () => calculateDaysRemaining( quotaResetDate ), @@ -104,11 +108,10 @@ export function PromptUsageProvider( { children }: PromptUsageProps ) { fetchPromptUsage, promptLimit, promptCount, - updatePromptUsage, userCanSendMessage: promptCount < promptLimit, daysUntilReset, }; - }, [ fetchPromptUsage, promptLimit, promptCount, updatePromptUsage, daysUntilReset ] ); + }, [ fetchPromptUsage, promptLimit, promptCount, daysUntilReset ] ); return { children }; } diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index 83a6e9180..5a4f40adc 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -13,7 +13,13 @@ const authTokenSchema = z.object( { expirationTime: z.number(), id: z.number(), email: z.string(), - displayName: z.string().optional(), + displayName: z.string().default( '' ), +} ); + +const meResponseSchema = z.object( { + ID: z.number(), + email: z.string(), + display_name: z.string(), } ); export type StoredToken = z.infer< typeof authTokenSchema >; @@ -81,7 +87,8 @@ async function handleAuthCallback( hash: string ): Promise< StoredToken > { throw new Error( 'Error while getting token' ); } - const response = await new wpcom( accessToken ).req.get( '/me?fields=ID,email,display_name' ); + const rawResponse = await new wpcom( accessToken ).req.get( '/me?fields=ID,email,display_name' ); + const response = meResponseSchema.parse( rawResponse ); return authTokenSchema.parse( { expiresIn, diff --git a/src/stores/chat-slice.ts b/src/stores/chat-slice.ts index c12c10bc4..b3108351e 100644 --- a/src/stores/chat-slice.ts +++ b/src/stores/chat-slice.ts @@ -1,6 +1,7 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import * as Sentry from '@sentry/electron/renderer'; import WPCOM from 'wpcom'; +import { z } from 'zod'; import { LOCAL_STORAGE_CHAT_API_IDS_KEY, LOCAL_STORAGE_CHAT_MESSAGES_KEY } from 'src/constants'; import { getIpcApi } from 'src/lib/get-ipc-api'; import { RootState } from 'src/stores'; @@ -11,7 +12,7 @@ export type Message = { messageApiId?: number; content: string; role: 'user' | 'assistant'; - chatApiId?: string; + chatApiId?: number; blocks?: { cliOutput?: string; cliStatus?: 'success' | 'error'; @@ -72,6 +73,29 @@ const updateFromSite = createAsyncThunk( } ); +const assistantResponseSchema = z.object( { + choices: z.array( + z.object( { + index: z.number(), + message: z.object( { + content: z.string(), + id: z.number(), + role: z.string(), + } ), + } ) + ), + created_at: z.string(), + id: z.number(), +} ); + +const assistantHeadersSchema = z.object( { + 'x-quota-max': z.string().default( '' ), + 'x-quota-remaining': z.string().default( '' ), +} ); + +type FetchAssistantResponseData = z.infer< typeof assistantResponseSchema >; +type FetchAssistantHeaders = z.infer< typeof assistantHeadersSchema >; + type FetchAssistantParams = { client: WPCOM; instanceId: string; @@ -80,11 +104,6 @@ type FetchAssistantParams = { siteId: string; }; -type FetchAssistantResponseData = { - choices: { message: { content: string; id: number } }[]; - id: string; -}; - const fetchAssistant = createAsyncThunk( 'chat/fetchAssistant', async ( { client, instanceId, siteId }: FetchAssistantParams, thunkAPI ) => { @@ -107,9 +126,9 @@ const fetchAssistant = createAsyncThunk( const { data, headers } = await new Promise< { data: FetchAssistantResponseData; - headers: Record< string, string >; + headers: FetchAssistantHeaders; } >( ( resolve, reject ) => { - client.req.post< FetchAssistantResponseData >( + client.req.post( { path: '/studio-app/ai-assistant/chat', apiNamespace: 'wpcom/v2', @@ -124,17 +143,26 @@ const fetchAssistant = createAsyncThunk( Sentry.captureException( error ); return reject( error ); } - return resolve( { data, headers } ); + + try { + const validatedData = assistantResponseSchema.parse( data ); + const validatedHeaders = assistantHeadersSchema.parse( headers ); + return resolve( { data: validatedData, headers: validatedHeaders } ); + } catch ( validationError ) { + Sentry.captureException( validationError ); + console.error( validationError ); + return reject( validationError ); + } } ); } ); return { - chatApiId: data?.id, - maxQuota: headers[ 'x-quota-max' ] || '', - message: data?.choices?.[ 0 ]?.message?.content, - messageApiId: data?.choices?.[ 0 ]?.message?.id, - remainingQuota: headers[ 'x-quota-remaining' ] || '', + chatApiId: data.id, + maxQuota: headers[ 'x-quota-max' ], + message: data.choices[ 0 ].message.content, + messageApiId: data.choices[ 0 ].message.id, + remainingQuota: headers[ 'x-quota-remaining' ], }; } ); @@ -182,10 +210,13 @@ export interface ChatState { availableEditors: string[]; wpVersion: string; messagesDict: { [ key: string ]: Message[] }; - chatApiIdDict: { [ key: string ]: string | undefined }; + chatApiIdDict: { [ key: string ]: number | undefined }; chatInputBySite: { [ key: string ]: string }; isLoadingDict: Record< string, boolean >; - promptUsageDict: Record< string, { maxQuota: string; remainingQuota: string } >; + promptUsage: { + maxQuota: string; + remainingQuota: string; + }; } const getInitialState = (): ChatState => { @@ -219,7 +250,10 @@ const getInitialState = (): ChatState => { chatApiIdDict: parsedStoredChatIds, chatInputBySite: {}, isLoadingDict: {}, - promptUsageDict: {}, + promptUsage: { + maxQuota: '', + remainingQuota: '', + }, }; }; @@ -227,14 +261,14 @@ export function generateMessage( content: string, role: 'user' | 'assistant', newMessageId: number, - chatApiId?: string, + chatApiId?: number, messageApiId?: number ): Message { return { content, role, id: newMessageId, - chatApiId, + chatApiId: chatApiId, createdAt: Date.now(), feedbackReceived: false, messageApiId, @@ -367,7 +401,7 @@ const chatSlice = createSlice( { state.chatApiIdDict[ instanceId ] = message.chatApiId; } - state.promptUsageDict[ instanceId ] = { + state.promptUsage = { maxQuota: action.payload.maxQuota, remainingQuota: action.payload.remainingQuota, }; diff --git a/src/stores/tests/chat-slice.test.ts b/src/stores/tests/chat-slice.test.ts index e6e8d7959..1d5c00f35 100644 --- a/src/stores/tests/chat-slice.test.ts +++ b/src/stores/tests/chat-slice.test.ts @@ -13,7 +13,8 @@ const mockClientReqPostUsingCallback = jest.fn().mockImplementation( ( params, c callback( null, { - id: 'chatcmpl-123', + created_at: '2025-02-06 12:00:00', + id: 123, choices: [ { message: { @@ -52,7 +53,7 @@ describe( 'chat-slice', () => { describe( 'fetchAssistant', () => { it( 'should add assistant message to state when fulfilled', async () => { const instanceId = 'test-site'; - const userMessage = generateMessage( 'Hello test 1', 'user', 0, 'chatcmpl-123', 42 ); + const userMessage = generateMessage( 'Hello test 1', 'user', 0, 100, 42 ); const result = await store.dispatch( chatThunks.fetchAssistant( { @@ -84,7 +85,7 @@ describe( 'chat-slice', () => { messageApiId: 42, } ); - expect( state.chat.promptUsageDict[ instanceId ] ).toEqual( { + expect( state.chat.promptUsage ).toEqual( { maxQuota: '100', remainingQuota: '99', } ); @@ -92,7 +93,7 @@ describe( 'chat-slice', () => { it( 'should update failed message when retrying', async () => { const instanceId = 'test-site'; - const userMessage = generateMessage( 'Hello test retry', 'user', 0, 'chatcmpl-123', 42 ); + const userMessage = generateMessage( 'Hello test retry', 'user', 0, 100, 42 ); userMessage.failedMessage = true; store.dispatch( chatActions.setMessages( { instanceId, messages: [ userMessage ] } ) ); @@ -129,11 +130,16 @@ describe( 'chat-slice', () => { chatApiId: 'chatcmpl-123', messageApiId: 42, } ); + + expect( state.chat.promptUsage ).toEqual( { + maxQuota: '100', + remainingQuota: '99', + } ); } ); it( 'should mark message as failed when rejected', async () => { const instanceId = 'test-site'; - const userMessage = generateMessage( 'Hello test 2', 'user', 0, 'chatcmpl-123', 42 ); + const userMessage = generateMessage( 'Hello test 2', 'user', 0, 100, 42 ); mockClientReqPostUsingCallback.mockImplementationOnce( ( params, callback ) => { callback( new Error( 'API Error' ), null, {} ); @@ -165,8 +171,8 @@ describe( 'chat-slice', () => { it( 'should mark message as feedback received', async () => { const instanceId = 'test-site'; - const userMessage = generateMessage( 'Hello test 3', 'user', 0, 'chatcmpl-123', 42 ); - const assistantMessage = generateMessage( 'Response', 'assistant', 1, 'chatcmpl-123', 43 ); + const userMessage = generateMessage( 'Hello test 3', 'user', 0, 100, 42 ); + const assistantMessage = generateMessage( 'Response', 'assistant', 1, 100, 43 ); store.dispatch( chatActions.setMessages( { instanceId, messages: [ userMessage, assistantMessage ] } ) ); @@ -192,7 +198,7 @@ describe( 'chat-slice', () => { describe( 'localStorage persistence', () => { it( 'should persist messagesDict and chatApiIdDict changes to localStorage', async () => { const instanceId = 'test-site'; - const userMessage = generateMessage( 'Hello test 4', 'user', 0, 'chatcmpl-123', 42 ); + const userMessage = generateMessage( 'Hello test 4', 'user', 0, 100, 42 ); await store.dispatch( chatThunks.fetchAssistant( { @@ -240,7 +246,7 @@ describe( 'chat-slice', () => { describe( 'updateMessage', () => { it( 'should update existing message block with CLI output', () => { const instanceId = 'test-site'; - const userMessage = generateMessage( 'Hello test 5', 'user', 0, 'chatcmpl-123', 42 ); + const userMessage = generateMessage( 'Hello test 5', 'user', 0, 100, 42 ); store.dispatch( chatActions.setMessages( { instanceId, messages: [ userMessage ] } ) ); store.dispatch( @@ -269,7 +275,7 @@ describe( 'chat-slice', () => { it( 'should update existing block when code content matches', () => { const instanceId = 'test-site'; const userMessage = { - ...generateMessage( 'Hello test 6', 'user', 0, 'chatcmpl-123', 42 ), + ...generateMessage( 'Hello test 6', 'user', 0, 100, 42 ), blocks: [ { codeBlockContent: 'wp plugin list', @@ -306,7 +312,7 @@ describe( 'chat-slice', () => { it( 'should add new block when code content is different', () => { const instanceId = 'test-site'; const userMessage = { - ...generateMessage( 'Hello test 7', 'user', 0, 'chatcmpl-123', 42 ), + ...generateMessage( 'Hello test 7', 'user', 0, 100, 42 ), blocks: [ { codeBlockContent: 'wp plugin list', From bd9d472785b45c6d677176b705d0ede7aff58233 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 6 Feb 2025 12:27:48 +0100 Subject: [PATCH 2/2] Fix test --- src/stores/tests/chat-slice.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/stores/tests/chat-slice.test.ts b/src/stores/tests/chat-slice.test.ts index 1d5c00f35..76e84fc97 100644 --- a/src/stores/tests/chat-slice.test.ts +++ b/src/stores/tests/chat-slice.test.ts @@ -17,9 +17,11 @@ const mockClientReqPostUsingCallback = jest.fn().mockImplementation( ( params, c id: 123, choices: [ { + index: 0, message: { id: 42, content: 'Test assistant response', + role: 'assistant', }, }, ], @@ -66,7 +68,7 @@ describe( 'chat-slice', () => { expect( result.type ).toBe( 'chat/fetchAssistant/fulfilled' ); expect( result.payload ).toEqual( { - chatApiId: 'chatcmpl-123', + chatApiId: 123, maxQuota: '100', message: 'Test assistant response', messageApiId: 42, @@ -81,7 +83,7 @@ describe( 'chat-slice', () => { expect( messages[ 1 ] ).toMatchObject( { content: 'Test assistant response', role: 'assistant', - chatApiId: 'chatcmpl-123', + chatApiId: 123, messageApiId: 42, } ); @@ -109,7 +111,7 @@ describe( 'chat-slice', () => { expect( result.type ).toBe( 'chat/fetchAssistant/fulfilled' ); expect( result.payload ).toEqual( { - chatApiId: 'chatcmpl-123', + chatApiId: 123, maxQuota: '100', message: 'Test assistant response', messageApiId: 42, @@ -127,7 +129,7 @@ describe( 'chat-slice', () => { expect( messages[ 1 ] ).toMatchObject( { content: 'Test assistant response', role: 'assistant', - chatApiId: 'chatcmpl-123', + chatApiId: 123, messageApiId: 42, } ); @@ -222,7 +224,7 @@ describe( 'chat-slice', () => { const storedChatIds = JSON.parse( localStorage.getItem( LOCAL_STORAGE_CHAT_API_IDS_KEY ) || '{}' ); - expect( storedChatIds[ instanceId ] ).toBe( 'chatcmpl-123' ); + expect( storedChatIds[ instanceId ] ).toBe( 123 ); } ); it( 'should handle invalid JSON in localStorage gracefully', () => {