2025-07-16 08:47:56 +08:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-08-26 00:04:53 +02:00
import { vi , describe , it , expect , beforeEach , afterEach } from 'vitest' ;
2025-10-03 00:52:16 +02:00
import type { SlashCommand , CommandContext } from './types.js' ;
2025-07-16 08:47:56 +08:00
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js' ;
2025-08-26 00:04:53 +02:00
import type { Content } from '@google/genai' ;
2025-11-13 15:13:39 -08:00
import { AuthType , type GeminiClient } from '@google/gemini-cli-core' ;
2025-07-16 08:47:56 +08:00
2025-08-25 22:11:27 +02:00
import * as fsPromises from 'node:fs/promises' ;
2026-01-08 03:43:55 -08:00
import { chatCommand } from './chatCommand.js' ;
import {
serializeHistoryToMarkdown ,
exportHistoryToFile ,
} from '../utils/historyExportUtils.js' ;
2025-08-26 00:04:53 +02:00
import type { Stats } from 'node:fs' ;
import type { HistoryItemWithoutId } from '../types.js' ;
2025-09-13 02:21:40 -04:00
import path from 'node:path' ;
2025-07-16 08:47:56 +08:00
vi . mock ( 'fs/promises' , ( ) = > ( {
stat : vi.fn ( ) ,
readdir : vi.fn ( ) . mockResolvedValue ( [ 'file1.txt' , 'file2.txt' ] as string [ ] ) ,
2025-09-13 02:21:40 -04:00
writeFile : vi.fn ( ) ,
2025-07-16 08:47:56 +08:00
} ) ) ;
2026-01-08 03:43:55 -08:00
vi . mock ( '../utils/historyExportUtils.js' , async ( importOriginal ) = > {
const actual =
await importOriginal < typeof import ( '../utils/historyExportUtils.js' ) > ( ) ;
return {
. . . actual ,
exportHistoryToFile : vi.fn ( ) ,
} ;
} ) ;
2025-07-16 08:47:56 +08:00
describe ( 'chatCommand' , ( ) = > {
2026-01-08 03:43:55 -08:00
const mockFs = vi . mocked ( fsPromises ) ;
const mockExport = vi . mocked ( exportHistoryToFile ) ;
2025-07-16 08:47:56 +08:00
let mockContext : CommandContext ;
let mockGetChat : ReturnType < typeof vi.fn > ;
let mockSaveCheckpoint : ReturnType < typeof vi.fn > ;
let mockLoadCheckpoint : ReturnType < typeof vi.fn > ;
2025-07-28 07:18:12 +09:00
let mockDeleteCheckpoint : ReturnType < typeof vi.fn > ;
2025-07-16 08:47:56 +08:00
let mockGetHistory : ReturnType < typeof vi.fn > ;
2025-07-28 07:18:12 +09:00
const getSubCommand = (
2025-09-13 02:21:40 -04:00
name : 'list' | 'save' | 'resume' | 'delete' | 'share' ,
2025-07-28 07:18:12 +09:00
) : SlashCommand = > {
2025-07-16 08:47:56 +08:00
const subCommand = chatCommand . subCommands ? . find (
( cmd ) = > cmd . name === name ,
) ;
if ( ! subCommand ) {
2025-07-28 07:18:12 +09:00
throw new Error ( ` /chat ${ name } command not found. ` ) ;
2025-07-16 08:47:56 +08:00
}
return subCommand ;
} ;
beforeEach ( ( ) = > {
mockGetHistory = vi . fn ( ) . mockReturnValue ( [ ] ) ;
2025-12-16 21:28:18 -08:00
mockGetChat = vi . fn ( ) . mockReturnValue ( {
2025-07-16 08:47:56 +08:00
getHistory : mockGetHistory ,
} ) ;
mockSaveCheckpoint = vi . fn ( ) . mockResolvedValue ( undefined ) ;
2025-11-13 15:13:39 -08:00
mockLoadCheckpoint = vi . fn ( ) . mockResolvedValue ( { history : [ ] } ) ;
2025-07-28 07:18:12 +09:00
mockDeleteCheckpoint = vi . fn ( ) . mockResolvedValue ( true ) ;
2025-07-16 08:47:56 +08:00
mockContext = createMockCommandContext ( {
services : {
config : {
2025-08-20 10:55:47 +09:00
getProjectRoot : ( ) = > '/project/root' ,
2025-07-16 08:47:56 +08:00
getGeminiClient : ( ) = >
( {
getChat : mockGetChat ,
} ) as unknown as GeminiClient ,
2025-08-20 10:55:47 +09:00
storage : {
getProjectTempDir : ( ) = > '/project/root/.gemini/tmp/mockhash' ,
} ,
2025-11-13 15:13:39 -08:00
getContentGeneratorConfig : ( ) = > ( {
authType : AuthType.LOGIN_WITH_GOOGLE ,
} ) ,
2025-07-16 08:47:56 +08:00
} ,
logger : {
saveCheckpoint : mockSaveCheckpoint ,
loadCheckpoint : mockLoadCheckpoint ,
2025-07-28 07:18:12 +09:00
deleteCheckpoint : mockDeleteCheckpoint ,
2025-07-16 08:47:56 +08:00
initialize : vi.fn ( ) . mockResolvedValue ( undefined ) ,
} ,
} ,
} ) ;
} ) ;
afterEach ( ( ) = > {
vi . restoreAllMocks ( ) ;
} ) ;
it ( 'should have the correct main command definition' , ( ) = > {
expect ( chatCommand . name ) . toBe ( 'chat' ) ;
2025-10-17 13:20:15 -07:00
expect ( chatCommand . description ) . toBe ( 'Manage conversation history' ) ;
2025-09-13 02:21:40 -04:00
expect ( chatCommand . subCommands ) . toHaveLength ( 5 ) ;
2025-07-16 08:47:56 +08:00
} ) ;
describe ( 'list subcommand' , ( ) = > {
let listCommand : SlashCommand ;
beforeEach ( ( ) = > {
listCommand = getSubCommand ( 'list' ) ;
} ) ;
2025-10-03 00:52:16 +02:00
it ( 'should add a chat_list item to the UI' , async ( ) = > {
2025-07-16 08:47:56 +08:00
const fakeFiles = [ 'checkpoint-test1.json' , 'checkpoint-test2.json' ] ;
2025-10-03 00:52:16 +02:00
const date1 = new Date ( ) ;
const date2 = new Date ( date1 . getTime ( ) + 1000 ) ;
2025-07-16 08:47:56 +08:00
2025-10-17 14:20:49 -07:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mockFs . readdir . mockResolvedValue ( fakeFiles as any ) ;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mockFs . stat . mockImplementation ( async ( path : any ) : Promise < Stats > = > {
2025-07-16 08:47:56 +08:00
if ( path . endsWith ( 'test1.json' ) ) {
2025-10-03 00:52:16 +02:00
return { mtime : date1 } as Stats ;
2025-07-16 08:47:56 +08:00
}
2025-10-03 00:52:16 +02:00
return { mtime : date2 } as Stats ;
} ) ;
2025-07-24 00:48:52 -04:00
2025-10-03 00:52:16 +02:00
await listCommand ? . action ? . ( mockContext , '' ) ;
expect ( mockContext . ui . addItem ) . toHaveBeenCalledWith (
{
type : 'chat_list' ,
chats : [
{
name : 'test1' ,
mtime : date1.toISOString ( ) ,
} ,
{
name : 'test2' ,
mtime : date2.toISOString ( ) ,
} ,
] ,
} ,
expect . any ( Number ) ,
) ;
2025-07-24 00:48:52 -04:00
} ) ;
2025-07-16 08:47:56 +08:00
} ) ;
describe ( 'save subcommand' , ( ) = > {
let saveCommand : SlashCommand ;
const tag = 'my-tag' ;
2025-08-09 15:59:22 +09:00
let mockCheckpointExists : ReturnType < typeof vi.fn > ;
2025-07-16 08:47:56 +08:00
beforeEach ( ( ) = > {
saveCommand = getSubCommand ( 'save' ) ;
2025-08-09 15:59:22 +09:00
mockCheckpointExists = vi . fn ( ) . mockResolvedValue ( false ) ;
mockContext . services . logger . checkpointExists = mockCheckpointExists ;
2025-07-16 08:47:56 +08:00
} ) ;
it ( 'should return an error if tag is missing' , async ( ) = > {
const result = await saveCommand ? . action ? . ( mockContext , ' ' ) ;
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'error' ,
content : 'Missing tag. Usage: /chat save <tag>' ,
} ) ;
} ) ;
2025-08-14 22:00:30 +05:30
it ( 'should inform if conversation history is empty or only contains system context' , async ( ) = > {
2025-07-16 08:47:56 +08:00
mockGetHistory . mockReturnValue ( [ ] ) ;
2025-08-14 22:00:30 +05:30
let result = await saveCommand ? . action ? . ( mockContext , tag ) ;
2025-07-16 08:47:56 +08:00
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'info' ,
content : 'No conversation found to save.' ,
} ) ;
2025-08-09 15:59:22 +09:00
2025-08-14 22:00:30 +05:30
mockGetHistory . mockReturnValue ( [
{ role : 'user' , parts : [ { text : 'context for our chat' } ] } ,
] ) ;
result = await saveCommand ? . action ? . ( mockContext , tag ) ;
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'info' ,
content : 'No conversation found to save.' ,
} ) ;
2025-08-09 15:59:22 +09:00
2025-08-14 22:00:30 +05:30
mockGetHistory . mockReturnValue ( [
{ role : 'user' , parts : [ { text : 'context for our chat' } ] } ,
{ role : 'model' , parts : [ { text : 'Got it. Thanks for the context!' } ] } ,
{ role : 'user' , parts : [ { text : 'Hello, how are you?' } ] } ,
] ) ;
result = await saveCommand ? . action ? . ( mockContext , tag ) ;
2025-08-09 15:59:22 +09:00
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'info' ,
content : ` Conversation checkpoint saved with tag: ${ tag } . ` ,
} ) ;
} ) ;
it ( 'should return confirm_action if checkpoint already exists' , async ( ) = > {
mockCheckpointExists . mockResolvedValue ( true ) ;
mockContext . invocation = {
raw : ` /chat save ${ tag } ` ,
name : 'save' ,
args : tag ,
} ;
const result = await saveCommand ? . action ? . ( mockContext , tag ) ;
expect ( mockCheckpointExists ) . toHaveBeenCalledWith ( tag ) ;
expect ( mockSaveCheckpoint ) . not . toHaveBeenCalled ( ) ;
expect ( result ) . toMatchObject ( {
type : 'confirm_action' ,
originalInvocation : { raw : ` /chat save ${ tag } ` } ,
} ) ;
// Check that prompt is a React element
expect ( result ) . toHaveProperty ( 'prompt' ) ;
} ) ;
it ( 'should save the conversation if overwrite is confirmed' , async ( ) = > {
2025-08-14 22:00:30 +05:30
const history : Content [ ] = [
{ role : 'user' , parts : [ { text : 'context for our chat' } ] } ,
{ role : 'user' , parts : [ { text : 'hello' } ] } ,
2025-07-16 08:47:56 +08:00
] ;
mockGetHistory . mockReturnValue ( history ) ;
2025-08-09 15:59:22 +09:00
mockContext . overwriteConfirmed = true ;
2025-07-16 08:47:56 +08:00
const result = await saveCommand ? . action ? . ( mockContext , tag ) ;
2025-08-09 15:59:22 +09:00
expect ( mockCheckpointExists ) . not . toHaveBeenCalled ( ) ; // Should skip existence check
2025-11-13 15:13:39 -08:00
expect ( mockSaveCheckpoint ) . toHaveBeenCalledWith (
{ history , authType : AuthType.LOGIN_WITH_GOOGLE } ,
tag ,
) ;
2025-07-16 08:47:56 +08:00
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'info' ,
content : ` Conversation checkpoint saved with tag: ${ tag } . ` ,
} ) ;
} ) ;
} ) ;
describe ( 'resume subcommand' , ( ) = > {
const goodTag = 'good-tag' ;
const badTag = 'bad-tag' ;
let resumeCommand : SlashCommand ;
beforeEach ( ( ) = > {
resumeCommand = getSubCommand ( 'resume' ) ;
} ) ;
it ( 'should return an error if tag is missing' , async ( ) = > {
const result = await resumeCommand ? . action ? . ( mockContext , '' ) ;
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'error' ,
content : 'Missing tag. Usage: /chat resume <tag>' ,
} ) ;
} ) ;
it ( 'should inform if checkpoint is not found' , async ( ) = > {
2025-11-13 15:13:39 -08:00
mockLoadCheckpoint . mockResolvedValue ( { history : [ ] } ) ;
2025-07-16 08:47:56 +08:00
const result = await resumeCommand ? . action ? . ( mockContext , badTag ) ;
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'info' ,
content : ` No saved checkpoint found with tag: ${ badTag } . ` ,
} ) ;
} ) ;
2025-11-13 15:13:39 -08:00
it ( 'should resume a conversation with matching authType' , async ( ) = > {
const conversation : Content [ ] = [
2025-12-17 14:04:02 -10:00
{ role : 'user' , parts : [ { text : 'system setup' } ] } ,
2025-11-13 15:13:39 -08:00
{ role : 'user' , parts : [ { text : 'hello gemini' } ] } ,
{ role : 'model' , parts : [ { text : 'hello world' } ] } ,
] ;
mockLoadCheckpoint . mockResolvedValue ( {
history : conversation ,
authType : AuthType.LOGIN_WITH_GOOGLE ,
} ) ;
const result = await resumeCommand ? . action ? . ( mockContext , goodTag ) ;
expect ( result ) . toEqual ( {
type : 'load_history' ,
history : [
{ type : 'user' , text : 'hello gemini' } ,
{ type : 'gemini' , text : 'hello world' } ,
] as HistoryItemWithoutId [ ] ,
clientHistory : conversation ,
} ) ;
} ) ;
it ( 'should block resuming a conversation with mismatched authType' , async ( ) = > {
const conversation : Content [ ] = [
2025-12-17 14:04:02 -10:00
{ role : 'user' , parts : [ { text : 'system setup' } ] } ,
2025-11-13 15:13:39 -08:00
{ role : 'user' , parts : [ { text : 'hello gemini' } ] } ,
{ role : 'model' , parts : [ { text : 'hello world' } ] } ,
] ;
mockLoadCheckpoint . mockResolvedValue ( {
history : conversation ,
authType : AuthType.USE_GEMINI ,
} ) ;
const result = await resumeCommand ? . action ? . ( mockContext , goodTag ) ;
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'error' ,
content : ` Cannot resume chat. It was saved with a different authentication method ( ${ AuthType . USE_GEMINI } ) than the current one ( ${ AuthType . LOGIN_WITH_GOOGLE } ). ` ,
} ) ;
} ) ;
it ( 'should resume a legacy conversation without authType' , async ( ) = > {
2025-07-16 08:47:56 +08:00
const conversation : Content [ ] = [
2025-12-17 14:04:02 -10:00
{ role : 'user' , parts : [ { text : 'system setup' } ] } ,
2025-07-16 08:47:56 +08:00
{ role : 'user' , parts : [ { text : 'hello gemini' } ] } ,
{ role : 'model' , parts : [ { text : 'hello world' } ] } ,
] ;
2025-11-13 15:13:39 -08:00
mockLoadCheckpoint . mockResolvedValue ( { history : conversation } ) ;
2025-07-16 08:47:56 +08:00
const result = await resumeCommand ? . action ? . ( mockContext , goodTag ) ;
expect ( result ) . toEqual ( {
type : 'load_history' ,
history : [
{ type : 'user' , text : 'hello gemini' } ,
{ type : 'gemini' , text : 'hello world' } ,
] as HistoryItemWithoutId [ ] ,
clientHistory : conversation ,
} ) ;
} ) ;
describe ( 'completion' , ( ) = > {
it ( 'should provide completion suggestions' , async ( ) = > {
const fakeFiles = [ 'checkpoint-alpha.json' , 'checkpoint-beta.json' ] ;
mockFs . readdir . mockImplementation (
( async ( _ : string ) : Promise < string [ ] > = >
2025-12-12 17:43:43 -08:00
fakeFiles ) as unknown as typeof fsPromises . readdir ,
2025-07-16 08:47:56 +08:00
) ;
mockFs . stat . mockImplementation (
( async ( _ : string ) : Promise < Stats > = >
( {
mtime : new Date ( ) ,
} ) as Stats ) as unknown as typeof fsPromises . stat ,
) ;
const result = await resumeCommand ? . completion ? . ( mockContext , 'a' ) ;
expect ( result ) . toEqual ( [ 'alpha' ] ) ;
} ) ;
it ( 'should suggest filenames sorted by modified time (newest first)' , async ( ) = > {
const fakeFiles = [ 'checkpoint-test1.json' , 'checkpoint-test2.json' ] ;
const date = new Date ( ) ;
mockFs . readdir . mockImplementation (
( async ( _ : string ) : Promise < string [ ] > = >
2025-12-12 17:43:43 -08:00
fakeFiles ) as unknown as typeof fsPromises . readdir ,
2025-07-16 08:47:56 +08:00
) ;
mockFs . stat . mockImplementation ( ( async (
path : string ,
) : Promise < Stats > = > {
if ( path . endsWith ( 'test1.json' ) ) {
return { mtime : date } as Stats ;
}
return { mtime : new Date ( date . getTime ( ) + 1000 ) } as Stats ;
} ) as unknown as typeof fsPromises . stat ) ;
const result = await resumeCommand ? . completion ? . ( mockContext , '' ) ;
// Sort items by last modified time (newest first)
expect ( result ) . toEqual ( [ 'test2' , 'test1' ] ) ;
} ) ;
} ) ;
} ) ;
2025-07-28 07:18:12 +09:00
describe ( 'delete subcommand' , ( ) = > {
let deleteCommand : SlashCommand ;
const tag = 'my-tag' ;
beforeEach ( ( ) = > {
deleteCommand = getSubCommand ( 'delete' ) ;
} ) ;
it ( 'should return an error if tag is missing' , async ( ) = > {
const result = await deleteCommand ? . action ? . ( mockContext , ' ' ) ;
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'error' ,
content : 'Missing tag. Usage: /chat delete <tag>' ,
} ) ;
} ) ;
it ( 'should return an error if checkpoint is not found' , async ( ) = > {
mockDeleteCheckpoint . mockResolvedValue ( false ) ;
const result = await deleteCommand ? . action ? . ( mockContext , tag ) ;
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'error' ,
content : ` Error: No checkpoint found with tag ' ${ tag } '. ` ,
} ) ;
} ) ;
it ( 'should delete the conversation' , async ( ) = > {
const result = await deleteCommand ? . action ? . ( mockContext , tag ) ;
expect ( mockDeleteCheckpoint ) . toHaveBeenCalledWith ( tag ) ;
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'info' ,
content : ` Conversation checkpoint ' ${ tag } ' has been deleted. ` ,
} ) ;
} ) ;
describe ( 'completion' , ( ) = > {
it ( 'should provide completion suggestions' , async ( ) = > {
const fakeFiles = [ 'checkpoint-alpha.json' , 'checkpoint-beta.json' ] ;
mockFs . readdir . mockImplementation (
( async ( _ : string ) : Promise < string [ ] > = >
2025-12-12 17:43:43 -08:00
fakeFiles ) as unknown as typeof fsPromises . readdir ,
2025-07-28 07:18:12 +09:00
) ;
mockFs . stat . mockImplementation (
( async ( _ : string ) : Promise < Stats > = >
( {
mtime : new Date ( ) ,
} ) as Stats ) as unknown as typeof fsPromises . stat ,
) ;
const result = await deleteCommand ? . completion ? . ( mockContext , 'a' ) ;
expect ( result ) . toEqual ( [ 'alpha' ] ) ;
} ) ;
} ) ;
} ) ;
2025-09-13 02:21:40 -04:00
describe ( 'share subcommand' , ( ) = > {
let shareCommand : SlashCommand ;
const mockHistory = [
{ role : 'user' , parts : [ { text : 'context' } ] } ,
{ role : 'model' , parts : [ { text : 'context response' } ] } ,
{ role : 'user' , parts : [ { text : 'Hello' } ] } ,
{ role : 'model' , parts : [ { text : 'Hi there!' } ] } ,
] ;
beforeEach ( ( ) = > {
shareCommand = getSubCommand ( 'share' ) ;
vi . spyOn ( process , 'cwd' ) . mockReturnValue (
path . resolve ( '/usr/local/google/home/myuser/gemini-cli' ) ,
) ;
vi . spyOn ( Date , 'now' ) . mockReturnValue ( 1234567890 ) ;
mockGetHistory . mockReturnValue ( mockHistory ) ;
mockFs . writeFile . mockClear ( ) ;
} ) ;
it ( 'should default to a json file if no path is provided' , async ( ) = > {
const result = await shareCommand ? . action ? . ( mockContext , '' ) ;
const expectedPath = path . join (
process . cwd ( ) ,
'gemini-conversation-1234567890.json' ,
) ;
2026-01-08 03:43:55 -08:00
expect ( mockExport ) . toHaveBeenCalledWith ( {
history : mockHistory ,
filePath : expectedPath ,
} ) ;
2025-09-13 02:21:40 -04:00
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'info' ,
content : ` Conversation shared to ${ expectedPath } ` ,
} ) ;
} ) ;
it ( 'should share the conversation to a JSON file' , async ( ) = > {
const filePath = 'my-chat.json' ;
const result = await shareCommand ? . action ? . ( mockContext , filePath ) ;
const expectedPath = path . join ( process . cwd ( ) , 'my-chat.json' ) ;
2026-01-08 03:43:55 -08:00
expect ( mockExport ) . toHaveBeenCalledWith ( {
history : mockHistory ,
filePath : expectedPath ,
} ) ;
2025-09-13 02:21:40 -04:00
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'info' ,
content : ` Conversation shared to ${ expectedPath } ` ,
} ) ;
} ) ;
it ( 'should share the conversation to a Markdown file' , async ( ) = > {
const filePath = 'my-chat.md' ;
const result = await shareCommand ? . action ? . ( mockContext , filePath ) ;
const expectedPath = path . join ( process . cwd ( ) , 'my-chat.md' ) ;
2026-01-08 03:43:55 -08:00
expect ( mockExport ) . toHaveBeenCalledWith ( {
history : mockHistory ,
filePath : expectedPath ,
} ) ;
2025-09-13 02:21:40 -04:00
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'info' ,
content : ` Conversation shared to ${ expectedPath } ` ,
} ) ;
} ) ;
it ( 'should return an error for unsupported file extensions' , async ( ) = > {
const filePath = 'my-chat.txt' ;
const result = await shareCommand ? . action ? . ( mockContext , filePath ) ;
2026-01-08 03:43:55 -08:00
expect ( mockExport ) . not . toHaveBeenCalled ( ) ;
2025-09-13 02:21:40 -04:00
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'error' ,
content : 'Invalid file format. Only .md and .json are supported.' ,
} ) ;
} ) ;
it ( 'should inform if there is no conversation to share' , async ( ) = > {
mockGetHistory . mockReturnValue ( [
{ role : 'user' , parts : [ { text : 'context' } ] } ,
] ) ;
const result = await shareCommand ? . action ? . ( mockContext , 'my-chat.json' ) ;
2026-01-08 03:43:55 -08:00
expect ( mockExport ) . not . toHaveBeenCalled ( ) ;
2025-09-13 02:21:40 -04:00
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'info' ,
content : 'No conversation found to share.' ,
} ) ;
} ) ;
it ( 'should handle errors during file writing' , async ( ) = > {
const error = new Error ( 'Permission denied' ) ;
2026-01-08 03:43:55 -08:00
mockExport . mockRejectedValue ( error ) ;
2025-09-13 02:21:40 -04:00
const result = await shareCommand ? . action ? . ( mockContext , 'my-chat.json' ) ;
expect ( result ) . toEqual ( {
type : 'message' ,
messageType : 'error' ,
content : ` Error sharing conversation: ${ error . message } ` ,
} ) ;
} ) ;
it ( 'should output valid JSON schema' , async ( ) = > {
const filePath = 'my-chat.json' ;
await shareCommand ? . action ? . ( mockContext , filePath ) ;
const expectedPath = path . join ( process . cwd ( ) , 'my-chat.json' ) ;
2026-01-08 03:43:55 -08:00
expect ( mockExport ) . toHaveBeenCalledWith ( {
history : mockHistory ,
filePath : expectedPath ,
2025-09-13 02:21:40 -04:00
} ) ;
} ) ;
it ( 'should output correct markdown format' , async ( ) = > {
const filePath = 'my-chat.md' ;
await shareCommand ? . action ? . ( mockContext , filePath ) ;
const expectedPath = path . join ( process . cwd ( ) , 'my-chat.md' ) ;
2026-01-08 03:43:55 -08:00
expect ( mockExport ) . toHaveBeenCalledWith ( {
history : mockHistory ,
filePath : expectedPath ,
2025-09-13 02:21:40 -04:00
} ) ;
} ) ;
} ) ;
describe ( 'serializeHistoryToMarkdown' , ( ) = > {
2025-09-19 17:47:20 -04:00
it ( 'should correctly serialize chat history to Markdown with icons' , ( ) = > {
2025-09-13 02:21:40 -04:00
const history : Content [ ] = [
{ role : 'user' , parts : [ { text : 'Hello' } ] } ,
{ role : 'model' , parts : [ { text : 'Hi there!' } ] } ,
{ role : 'user' , parts : [ { text : 'How are you?' } ] } ,
] ;
const expectedMarkdown =
2025-12-04 10:30:29 +11:00
'## USER 🧑💻\n\nHello\n\n---\n\n' +
'## MODEL ✨\n\nHi there!\n\n---\n\n' +
'## USER 🧑💻\n\nHow are you?' ;
2025-09-13 02:21:40 -04:00
const result = serializeHistoryToMarkdown ( history ) ;
expect ( result ) . toBe ( expectedMarkdown ) ;
} ) ;
it ( 'should handle empty history' , ( ) = > {
const history : Content [ ] = [ ] ;
const result = serializeHistoryToMarkdown ( history ) ;
expect ( result ) . toBe ( '' ) ;
} ) ;
it ( 'should handle items with no text parts' , ( ) = > {
const history : Content [ ] = [
{ role : 'user' , parts : [ { text : 'Hello' } ] } ,
{ role : 'model' , parts : [ ] } ,
{ role : 'user' , parts : [ { text : 'How are you?' } ] } ,
] ;
2025-12-04 10:30:29 +11:00
const expectedMarkdown = ` ## USER 🧑💻
2025-09-19 17:47:20 -04:00
Hello
-- -
2025-12-04 10:30:29 +11:00
# # MODEL ✨
2025-09-19 17:47:20 -04:00
-- -
2025-12-04 10:30:29 +11:00
# # USER 🧑 💻
2025-09-19 17:47:20 -04:00
How are you ? ` ;
const result = serializeHistoryToMarkdown ( history ) ;
expect ( result ) . toBe ( expectedMarkdown ) ;
} ) ;
it ( 'should correctly serialize function calls and responses' , ( ) = > {
const history : Content [ ] = [
{
role : 'user' ,
parts : [ { text : 'Please call a function.' } ] ,
} ,
{
role : 'model' ,
parts : [
{
functionCall : {
name : 'my-function' ,
args : { arg1 : 'value1' } ,
} ,
} ,
] ,
} ,
{
role : 'user' ,
parts : [
{
functionResponse : {
name : 'my-function' ,
response : { result : 'success' } ,
} ,
} ,
] ,
} ,
] ;
2025-12-04 10:30:29 +11:00
const expectedMarkdown = ` ## USER 🧑💻
2025-09-19 17:47:20 -04:00
Please call a function .
-- -
2025-12-04 10:30:29 +11:00
# # MODEL ✨
2025-09-19 17:47:20 -04:00
* * Tool Command * * :
\ ` \` \` json
{
"name" : "my-function" ,
"args" : {
"arg1" : "value1"
}
}
\ ` \` \`
-- -
2025-12-04 10:30:29 +11:00
# # USER 🧑 💻
2025-09-19 17:47:20 -04:00
* * Tool Response * * :
\ ` \` \` json
{
"name" : "my-function" ,
"response" : {
"result" : "success"
}
}
\ ` \` \` ` ;
2025-09-13 02:21:40 -04:00
const result = serializeHistoryToMarkdown ( history ) ;
expect ( result ) . toBe ( expectedMarkdown ) ;
} ) ;
2025-09-19 17:47:20 -04:00
it ( 'should handle items with undefined role' , ( ) = > {
const history : Array < Partial < Content > > = [
{ role : 'user' , parts : [ { text : 'Hello' } ] } ,
{ parts : [ { text : 'Hi there!' } ] } ,
] ;
2025-12-04 10:30:29 +11:00
const expectedMarkdown = ` ## USER 🧑💻
2025-09-19 17:47:20 -04:00
Hello
-- -
2025-12-04 10:30:29 +11:00
# # MODEL ✨
2025-09-19 17:47:20 -04:00
Hi there ! ` ;
const result = serializeHistoryToMarkdown ( history as Content [ ] ) ;
expect ( result ) . toBe ( expectedMarkdown ) ;
} ) ;
2025-09-13 02:21:40 -04:00
} ) ;
2025-07-16 08:47:56 +08:00
} ) ;