@machina.ai/cell-cli 1.36.0-rc1 → 1.38.1-rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/package.json +4 -4
- package/dist/src/acp/acpClient.js +297 -32
- package/dist/src/acp/acpClient.js.map +1 -1
- package/dist/src/acp/acpClient.test.js +419 -19
- package/dist/src/acp/acpClient.test.js.map +1 -1
- package/dist/src/acp/acpResume.test.js +8 -0
- package/dist/src/acp/acpResume.test.js.map +1 -1
- package/dist/src/acp/commandHandler.js +4 -0
- package/dist/src/acp/commandHandler.js.map +1 -1
- package/dist/src/acp/commandHandler.test.js +4 -0
- package/dist/src/acp/commandHandler.test.js.map +1 -1
- package/dist/src/acp/commands/about.d.ts +11 -0
- package/dist/src/acp/commands/about.js +53 -0
- package/dist/src/acp/commands/about.js.map +1 -0
- package/dist/src/acp/commands/extensions.js +1 -1
- package/dist/src/acp/commands/extensions.js.map +1 -1
- package/dist/src/acp/commands/help.d.ts +14 -0
- package/dist/src/acp/commands/help.js +35 -0
- package/dist/src/acp/commands/help.js.map +1 -0
- package/dist/src/acp/commands/help.test.d.ts +6 -0
- package/dist/src/acp/commands/help.test.js +40 -0
- package/dist/src/acp/commands/help.test.js.map +1 -0
- package/dist/src/acp/commands/restore.js +2 -2
- package/dist/src/acp/commands/restore.js.map +1 -1
- package/dist/src/commands/extensions/new.js +1 -1
- package/dist/src/commands/extensions/new.js.map +1 -1
- package/dist/src/commands/mcp/list.js +2 -2
- package/dist/src/commands/mcp/list.js.map +1 -1
- package/dist/src/commands/mcp.test.js +1 -1
- package/dist/src/commands/mcp.test.js.map +1 -1
- package/dist/src/commands/skills/list.js +5 -8
- package/dist/src/commands/skills/list.js.map +1 -1
- package/dist/src/commands/skills/list.test.js +17 -13
- package/dist/src/commands/skills/list.test.js.map +1 -1
- package/dist/src/config/config.js +22 -9
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +121 -36
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/extension-manager-permissions.test.js +1 -1
- package/dist/src/config/extension-manager-permissions.test.js.map +1 -1
- package/dist/src/config/extension-manager-themes.spec.js +1 -0
- package/dist/src/config/extension-manager-themes.spec.js.map +1 -1
- package/dist/src/config/extension-manager.test.js +1 -1
- package/dist/src/config/extension-manager.test.js.map +1 -1
- package/dist/src/config/extension.js +1 -1
- package/dist/src/config/extension.js.map +1 -1
- package/dist/src/config/extensions/github.js +1 -1
- package/dist/src/config/extensions/github.js.map +1 -1
- package/dist/src/config/footerItems.d.ts +4 -0
- package/dist/src/config/footerItems.js +12 -2
- package/dist/src/config/footerItems.js.map +1 -1
- package/dist/src/config/footerItems.test.js +129 -72
- package/dist/src/config/footerItems.test.js.map +1 -1
- package/dist/src/config/policy-engine.integration.test.js +1 -3
- package/dist/src/config/policy-engine.integration.test.js.map +1 -1
- package/dist/src/config/policy.d.ts +1 -1
- package/dist/src/config/policy.js +2 -2
- package/dist/src/config/policy.js.map +1 -1
- package/dist/src/config/settings.js +19 -3
- package/dist/src/config/settings.js.map +1 -1
- package/dist/src/config/settingsSchema.d.ts +272 -53
- package/dist/src/config/settingsSchema.js +262 -48
- package/dist/src/config/settingsSchema.js.map +1 -1
- package/dist/src/config/settingsSchema.test.js +22 -4
- package/dist/src/config/settingsSchema.test.js.map +1 -1
- package/dist/src/config/workspace-policy-cli.test.js +7 -7
- package/dist/src/config/workspace-policy-cli.test.js.map +1 -1
- package/dist/src/gemini.js +24 -7
- package/dist/src/gemini.js.map +1 -1
- package/dist/src/gemini.test.js +74 -4
- package/dist/src/gemini.test.js.map +1 -1
- package/dist/src/gemini_cleanup.test.js +69 -4
- package/dist/src/gemini_cleanup.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/integration-tests/modelSteering.test.js +1 -1
- package/dist/src/integration-tests/modelSteering.test.js.map +1 -1
- package/dist/src/interactiveCli.js +4 -2
- package/dist/src/interactiveCli.js.map +1 -1
- package/dist/src/nonInteractiveCli.d.ts +1 -1
- package/dist/src/nonInteractiveCli.js +11 -2
- package/dist/src/nonInteractiveCli.js.map +1 -1
- package/dist/src/nonInteractiveCli.test.js +4 -2
- package/dist/src/nonInteractiveCli.test.js.map +1 -1
- package/dist/src/nonInteractiveCliAgentSession.d.ts +16 -0
- package/dist/src/nonInteractiveCliAgentSession.js +484 -0
- package/dist/src/nonInteractiveCliAgentSession.js.map +1 -0
- package/dist/src/nonInteractiveCliAgentSession.test.js +1837 -0
- package/dist/src/nonInteractiveCliAgentSession.test.js.map +1 -0
- package/dist/src/services/BuiltinCommandLoader.js +4 -2
- package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
- package/dist/src/test-utils/mockCommandContext.js +1 -0
- package/dist/src/test-utils/mockCommandContext.js.map +1 -1
- package/dist/src/test-utils/mockConfig.js +16 -0
- package/dist/src/test-utils/mockConfig.js.map +1 -1
- package/dist/src/test-utils/mockSpinner.d.ts +6 -0
- package/dist/src/test-utils/mockSpinner.js +21 -0
- package/dist/src/test-utils/mockSpinner.js.map +1 -0
- package/dist/src/test-utils/render.d.ts +8 -1
- package/dist/src/test-utils/render.js +37 -11
- package/dist/src/test-utils/render.js.map +1 -1
- package/dist/src/ui/App.test.js +1 -1
- package/dist/src/ui/App.test.js.map +1 -1
- package/dist/src/ui/AppContainer.js +225 -99
- package/dist/src/ui/AppContainer.js.map +1 -1
- package/dist/src/ui/AppContainer.test.js +50 -38
- package/dist/src/ui/AppContainer.test.js.map +1 -1
- package/dist/src/ui/commands/chatCommand.js +15 -5
- package/dist/src/ui/commands/chatCommand.js.map +1 -1
- package/dist/src/ui/commands/clearCommand.js +3 -1
- package/dist/src/ui/commands/clearCommand.js.map +1 -1
- package/dist/src/ui/commands/directoryCommand.js +1 -1
- package/dist/src/ui/commands/directoryCommand.js.map +1 -1
- package/dist/src/ui/commands/extensionsCommand.js +22 -11
- package/dist/src/ui/commands/extensionsCommand.js.map +1 -1
- package/dist/src/ui/commands/marketplaceCommand.d.ts +7 -0
- package/dist/src/ui/commands/marketplaceCommand.js +135 -0
- package/dist/src/ui/commands/marketplaceCommand.js.map +1 -0
- package/dist/src/ui/commands/mcpCommand.js +26 -7
- package/dist/src/ui/commands/mcpCommand.js.map +1 -1
- package/dist/src/ui/commands/mcpCommand.test.js +26 -0
- package/dist/src/ui/commands/mcpCommand.test.js.map +1 -1
- package/dist/src/ui/commands/planCommand.js +9 -0
- package/dist/src/ui/commands/planCommand.js.map +1 -1
- package/dist/src/ui/commands/planCommand.test.js +29 -0
- package/dist/src/ui/commands/planCommand.test.js.map +1 -1
- package/dist/src/ui/commands/restoreCommand.js +1 -1
- package/dist/src/ui/commands/restoreCommand.js.map +1 -1
- package/dist/src/ui/commands/rewindCommand.js +3 -1
- package/dist/src/ui/commands/rewindCommand.js.map +1 -1
- package/dist/src/ui/commands/rewindCommand.test.js +1 -1
- package/dist/src/ui/commands/rewindCommand.test.js.map +1 -1
- package/dist/src/ui/commands/setupGithubCommand.js +5 -5
- package/dist/src/ui/commands/setupGithubCommand.js.map +1 -1
- package/dist/src/ui/commands/skillsCommand.js +11 -1
- package/dist/src/ui/commands/skillsCommand.js.map +1 -1
- package/dist/src/ui/commands/skillsCommand.test.js +1 -0
- package/dist/src/ui/commands/skillsCommand.test.js.map +1 -1
- package/dist/src/ui/commands/{shellsCommand.d.ts → tasksCommand.d.ts} +1 -1
- package/dist/src/ui/commands/{shellsCommand.js → tasksCommand.js} +6 -6
- package/dist/src/ui/commands/tasksCommand.js.map +1 -0
- package/dist/src/ui/commands/tasksCommand.test.js +30 -0
- package/dist/src/ui/commands/tasksCommand.test.js.map +1 -0
- package/dist/src/ui/commands/types.d.ts +9 -1
- package/dist/src/ui/components/AnsiOutput.js +7 -5
- package/dist/src/ui/components/AnsiOutput.js.map +1 -1
- package/dist/src/ui/components/AnsiOutput.test.js +13 -0
- package/dist/src/ui/components/AnsiOutput.test.js.map +1 -1
- package/dist/src/ui/components/AppHeader.js +11 -11
- package/dist/src/ui/components/AppHeader.js.map +1 -1
- package/dist/src/ui/components/AppHeader.test.js +6 -2
- package/dist/src/ui/components/AppHeader.test.js.map +1 -1
- package/dist/src/ui/components/AsciiArt.d.ts +6 -6
- package/dist/src/ui/components/AsciiArt.js +6 -6
- package/dist/src/ui/components/AskUserDialog.js +13 -13
- package/dist/src/ui/components/AskUserDialog.js.map +1 -1
- package/dist/src/ui/components/AskUserDialog.test.js +31 -0
- package/dist/src/ui/components/AskUserDialog.test.js.map +1 -1
- package/dist/src/ui/components/BackgroundTaskDisplay.d.ts +16 -0
- package/dist/src/ui/components/{BackgroundShellDisplay.js → BackgroundTaskDisplay.js} +12 -12
- package/dist/src/ui/components/BackgroundTaskDisplay.js.map +1 -0
- package/dist/src/ui/components/{BackgroundShellDisplay.test.js → BackgroundTaskDisplay.test.js} +26 -26
- package/dist/src/ui/components/BackgroundTaskDisplay.test.js.map +1 -0
- package/dist/src/ui/components/Composer.js +20 -208
- package/dist/src/ui/components/Composer.js.map +1 -1
- package/dist/src/ui/components/Composer.test.js +22 -17
- package/dist/src/ui/components/Composer.test.js.map +1 -1
- package/dist/src/ui/components/ContextSummaryDisplay.js +3 -1
- package/dist/src/ui/components/ContextSummaryDisplay.js.map +1 -1
- package/dist/src/ui/components/CopyModeWarning.js +3 -6
- package/dist/src/ui/components/CopyModeWarning.js.map +1 -1
- package/dist/src/ui/components/CopyModeWarning.test.js +7 -8
- package/dist/src/ui/components/CopyModeWarning.test.js.map +1 -1
- package/dist/src/ui/components/ExitPlanModeDialog.js +9 -0
- package/dist/src/ui/components/ExitPlanModeDialog.js.map +1 -1
- package/dist/src/ui/components/ExitPlanModeDialog.test.js +15 -3
- package/dist/src/ui/components/ExitPlanModeDialog.test.js.map +1 -1
- package/dist/src/ui/components/FolderTrustDialog.test.js +7 -8
- package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
- package/dist/src/ui/components/Footer.js +36 -9
- package/dist/src/ui/components/Footer.js.map +1 -1
- package/dist/src/ui/components/Footer.test.js +85 -6
- package/dist/src/ui/components/Footer.test.js.map +1 -1
- package/dist/src/ui/components/FooterConfigDialog.js +1 -0
- package/dist/src/ui/components/FooterConfigDialog.js.map +1 -1
- package/dist/src/ui/components/FooterConfigDialog.test.js +1 -1
- package/dist/src/ui/components/Help.test.js +1 -1
- package/dist/src/ui/components/Help.test.js.map +1 -1
- package/dist/src/ui/components/HistoryItemDisplay.d.ts +2 -0
- package/dist/src/ui/components/HistoryItemDisplay.js +14 -12
- package/dist/src/ui/components/HistoryItemDisplay.js.map +1 -1
- package/dist/src/ui/components/InputPrompt.d.ts +10 -5
- package/dist/src/ui/components/InputPrompt.js +195 -90
- package/dist/src/ui/components/InputPrompt.js.map +1 -1
- package/dist/src/ui/components/InputPrompt.test.d.ts +12 -1
- package/dist/src/ui/components/InputPrompt.test.js +370 -160
- package/dist/src/ui/components/InputPrompt.test.js.map +1 -1
- package/dist/src/ui/components/LoadingIndicator.js +1 -2
- package/dist/src/ui/components/LoadingIndicator.js.map +1 -1
- package/dist/src/ui/components/LoadingIndicator.test.js +7 -0
- package/dist/src/ui/components/LoadingIndicator.test.js.map +1 -1
- package/dist/src/ui/components/MainContent.d.ts +1 -1
- package/dist/src/ui/components/MainContent.js +105 -34
- package/dist/src/ui/components/MainContent.js.map +1 -1
- package/dist/src/ui/components/MainContent.test.js +12 -9
- package/dist/src/ui/components/MainContent.test.js.map +1 -1
- package/dist/src/ui/components/MemoryUsageDisplay.d.ts +1 -0
- package/dist/src/ui/components/MemoryUsageDisplay.js +5 -2
- package/dist/src/ui/components/MemoryUsageDisplay.js.map +1 -1
- package/dist/src/ui/components/ModelDialog.js +50 -72
- package/dist/src/ui/components/ModelDialog.js.map +1 -1
- package/dist/src/ui/components/ModelDialog.test.js +1 -0
- package/dist/src/ui/components/ModelDialog.test.js.map +1 -1
- package/dist/src/ui/components/ModelQuotaDisplay.d.ts +18 -0
- package/dist/src/ui/components/ModelQuotaDisplay.js +104 -0
- package/dist/src/ui/components/ModelQuotaDisplay.js.map +1 -0
- package/dist/src/ui/components/ModelQuotaDisplay.test.d.ts +6 -0
- package/dist/src/ui/components/ModelQuotaDisplay.test.js +62 -0
- package/dist/src/ui/components/ModelQuotaDisplay.test.js.map +1 -0
- package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +1 -1
- package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +1 -1
- package/dist/src/ui/components/ProgressBar.d.ts +13 -0
- package/dist/src/ui/components/ProgressBar.js +17 -0
- package/dist/src/ui/components/ProgressBar.js.map +1 -0
- package/dist/src/ui/components/ProgressBar.test.d.ts +6 -0
- package/dist/src/ui/components/ProgressBar.test.js +28 -0
- package/dist/src/ui/components/ProgressBar.test.js.map +1 -0
- package/dist/src/ui/components/StatsDisplay.d.ts +2 -2
- package/dist/src/ui/components/StatsDisplay.js +47 -128
- package/dist/src/ui/components/StatsDisplay.js.map +1 -1
- package/dist/src/ui/components/StatsDisplay.test.js +65 -136
- package/dist/src/ui/components/StatsDisplay.test.js.map +1 -1
- package/dist/src/ui/components/StatusDisplay.js +1 -1
- package/dist/src/ui/components/StatusDisplay.js.map +1 -1
- package/dist/src/ui/components/StatusDisplay.test.js +3 -3
- package/dist/src/ui/components/StatusDisplay.test.js.map +1 -1
- package/dist/src/ui/components/StatusRow.d.ts +32 -0
- package/dist/src/ui/components/StatusRow.js +180 -0
- package/dist/src/ui/components/StatusRow.js.map +1 -0
- package/dist/src/ui/components/StatusRow.test.d.ts +6 -0
- package/dist/src/ui/components/StatusRow.test.js +99 -0
- package/dist/src/ui/components/StatusRow.test.js.map +1 -0
- package/dist/src/ui/components/ToastDisplay.d.ts +2 -1
- package/dist/src/ui/components/ToastDisplay.js +7 -5
- package/dist/src/ui/components/ToastDisplay.js.map +1 -1
- package/dist/src/ui/components/ToastDisplay.test.js +34 -20
- package/dist/src/ui/components/ToastDisplay.test.js.map +1 -1
- package/dist/src/ui/components/ToolConfirmationQueue.js +24 -9
- package/dist/src/ui/components/ToolConfirmationQueue.js.map +1 -1
- package/dist/src/ui/components/ToolConfirmationQueue.test.js +4 -6
- package/dist/src/ui/components/ToolConfirmationQueue.test.js.map +1 -1
- package/dist/src/ui/components/UserIdentity.js +8 -5
- package/dist/src/ui/components/UserIdentity.js.map +1 -1
- package/dist/src/ui/components/messages/DenseToolMessage.d.ts +13 -0
- package/dist/src/ui/components/messages/DenseToolMessage.js +270 -0
- package/dist/src/ui/components/messages/DenseToolMessage.js.map +1 -0
- package/dist/src/ui/components/messages/DenseToolMessage.test.d.ts +6 -0
- package/dist/src/ui/components/messages/DenseToolMessage.test.js +383 -0
- package/dist/src/ui/components/messages/DenseToolMessage.test.js.map +1 -0
- package/dist/src/ui/components/messages/DiffRenderer.d.ts +18 -0
- package/dist/src/ui/components/messages/DiffRenderer.js +54 -34
- package/dist/src/ui/components/messages/DiffRenderer.js.map +1 -1
- package/dist/src/ui/components/messages/DiffRenderer.test.js +12 -6
- package/dist/src/ui/components/messages/DiffRenderer.test.js.map +1 -1
- package/dist/src/ui/components/messages/InfoMessage.d.ts +1 -0
- package/dist/src/ui/components/messages/InfoMessage.js +2 -2
- package/dist/src/ui/components/messages/InfoMessage.js.map +1 -1
- package/dist/src/ui/components/messages/RedirectionConfirmation.test.js +1 -1
- package/dist/src/ui/components/messages/RedirectionConfirmation.test.js.map +1 -1
- package/dist/src/ui/components/messages/ShellToolMessage.test.js +44 -33
- package/dist/src/ui/components/messages/ShellToolMessage.test.js.map +1 -1
- package/dist/src/ui/components/messages/SubagentGroupDisplay.js +3 -2
- package/dist/src/ui/components/messages/SubagentGroupDisplay.js.map +1 -1
- package/dist/src/ui/components/messages/SubagentGroupDisplay.test.js +1 -1
- package/dist/src/ui/components/messages/SubagentGroupDisplay.test.js.map +1 -1
- package/dist/src/ui/components/messages/SubagentHistoryMessage.d.ts +13 -0
- package/dist/src/ui/components/messages/SubagentHistoryMessage.js +4 -0
- package/dist/src/ui/components/messages/SubagentHistoryMessage.js.map +1 -0
- package/dist/src/ui/components/messages/SubagentHistoryMessage.test.d.ts +6 -0
- package/dist/src/ui/components/messages/SubagentHistoryMessage.test.js +68 -0
- package/dist/src/ui/components/messages/SubagentHistoryMessage.test.js.map +1 -0
- package/dist/src/ui/components/messages/SubagentProgressDisplay.d.ts +2 -1
- package/dist/src/ui/components/messages/SubagentProgressDisplay.js +2 -2
- package/dist/src/ui/components/messages/SubagentProgressDisplay.js.map +1 -1
- package/dist/src/ui/components/messages/SubagentProgressDisplay.test.js +0 -4
- package/dist/src/ui/components/messages/SubagentProgressDisplay.test.js.map +1 -1
- package/dist/src/ui/components/messages/ToolConfirmationMessage.d.ts +1 -0
- package/dist/src/ui/components/messages/ToolConfirmationMessage.js +108 -70
- package/dist/src/ui/components/messages/ToolConfirmationMessage.js.map +1 -1
- package/dist/src/ui/components/messages/ToolConfirmationMessage.test.js +40 -25
- package/dist/src/ui/components/messages/ToolConfirmationMessage.test.js.map +1 -1
- package/dist/src/ui/components/messages/ToolGroupMessage.compact.test.d.ts +6 -0
- package/dist/src/ui/components/messages/ToolGroupMessage.compact.test.js +147 -0
- package/dist/src/ui/components/messages/ToolGroupMessage.compact.test.js.map +1 -0
- package/dist/src/ui/components/messages/ToolGroupMessage.d.ts +3 -0
- package/dist/src/ui/components/messages/ToolGroupMessage.js +219 -52
- package/dist/src/ui/components/messages/ToolGroupMessage.js.map +1 -1
- package/dist/src/ui/components/messages/ToolGroupMessage.test.js +55 -3
- package/dist/src/ui/components/messages/ToolGroupMessage.test.js.map +1 -1
- package/dist/src/ui/components/messages/ToolMessage.test.js +8 -7
- package/dist/src/ui/components/messages/ToolMessage.test.js.map +1 -1
- package/dist/src/ui/components/messages/ToolOverflowConsistencyChecks.test.js +1 -1
- package/dist/src/ui/components/messages/ToolOverflowConsistencyChecks.test.js.map +1 -1
- package/dist/src/ui/components/messages/ToolResultDisplay.js +57 -15
- package/dist/src/ui/components/messages/ToolResultDisplay.js.map +1 -1
- package/dist/src/ui/components/messages/ToolResultDisplay.test.js +66 -3
- package/dist/src/ui/components/messages/ToolResultDisplay.test.js.map +1 -1
- package/dist/src/ui/components/messages/ToolResultDisplayOverflow.test.js +5 -4
- package/dist/src/ui/components/messages/ToolResultDisplayOverflow.test.js.map +1 -1
- package/dist/src/ui/components/messages/ToolStickyHeaderRegression.test.js +3 -3
- package/dist/src/ui/components/messages/ToolStickyHeaderRegression.test.js.map +1 -1
- package/dist/src/ui/components/messages/TopicMessage.d.ts +15 -0
- package/dist/src/ui/components/messages/TopicMessage.js +56 -0
- package/dist/src/ui/components/messages/TopicMessage.js.map +1 -0
- package/dist/src/ui/components/messages/TopicMessage.test.d.ts +6 -0
- package/dist/src/ui/components/messages/TopicMessage.test.js +77 -0
- package/dist/src/ui/components/messages/TopicMessage.test.js.map +1 -0
- package/dist/src/ui/components/shared/MaxSizedBox.d.ts +1 -0
- package/dist/src/ui/components/shared/MaxSizedBox.js +10 -7
- package/dist/src/ui/components/shared/MaxSizedBox.js.map +1 -1
- package/dist/src/ui/components/shared/Scrollable.d.ts +3 -0
- package/dist/src/ui/components/shared/Scrollable.js +6 -2
- package/dist/src/ui/components/shared/Scrollable.js.map +1 -1
- package/dist/src/ui/components/shared/ScrollableList.d.ts +9 -12
- package/dist/src/ui/components/shared/ScrollableList.js +2 -2
- package/dist/src/ui/components/shared/ScrollableList.js.map +1 -1
- package/dist/src/ui/components/shared/VirtualizedList.d.ts +13 -1
- package/dist/src/ui/components/shared/VirtualizedList.js +148 -37
- package/dist/src/ui/components/shared/VirtualizedList.js.map +1 -1
- package/dist/src/ui/components/shared/VirtualizedList.test.js +1 -10
- package/dist/src/ui/components/shared/VirtualizedList.test.js.map +1 -1
- package/dist/src/ui/components/shared/text-buffer.d.ts +1 -0
- package/dist/src/ui/components/shared/text-buffer.js +19 -21
- package/dist/src/ui/components/shared/text-buffer.js.map +1 -1
- package/dist/src/ui/components/views/ExtensionDetails.d.ts +4 -1
- package/dist/src/ui/components/views/ExtensionDetails.js +14 -4
- package/dist/src/ui/components/views/ExtensionDetails.js.map +1 -1
- package/dist/src/ui/components/views/ExtensionDetails.test.js +25 -1
- package/dist/src/ui/components/views/ExtensionDetails.test.js.map +1 -1
- package/dist/src/ui/components/views/ExtensionRegistryView.js +19 -5
- package/dist/src/ui/components/views/ExtensionRegistryView.js.map +1 -1
- package/dist/src/ui/components/views/ExtensionRegistryView.test.js +38 -0
- package/dist/src/ui/components/views/ExtensionRegistryView.test.js.map +1 -1
- package/dist/src/ui/components/views/SkillsList.js +2 -1
- package/dist/src/ui/components/views/SkillsList.js.map +1 -1
- package/dist/src/ui/components/views/SkillsList.test.js +3 -1
- package/dist/src/ui/components/views/SkillsList.test.js.map +1 -1
- package/dist/src/ui/constants/tips.js +2 -2
- package/dist/src/ui/constants/tips.js.map +1 -1
- package/dist/src/ui/constants.d.ts +6 -0
- package/dist/src/ui/constants.js +15 -0
- package/dist/src/ui/constants.js.map +1 -1
- package/dist/src/ui/contexts/InputContext.d.ts +17 -0
- package/dist/src/ui/contexts/InputContext.js +15 -0
- package/dist/src/ui/contexts/InputContext.js.map +1 -0
- package/dist/src/ui/contexts/KeypressContext.js +1 -1
- package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.test.js +25 -1
- package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
- package/dist/src/ui/contexts/ScrollProvider.js +25 -3
- package/dist/src/ui/contexts/ScrollProvider.js.map +1 -1
- package/dist/src/ui/contexts/ScrollProvider.test.js +100 -0
- package/dist/src/ui/contexts/ScrollProvider.test.js.map +1 -1
- package/dist/src/ui/contexts/SessionContext.d.ts +2 -2
- package/dist/src/ui/contexts/SessionContext.js.map +1 -1
- package/dist/src/ui/contexts/ToolActionsContext.d.ts +6 -0
- package/dist/src/ui/contexts/ToolActionsContext.js +19 -11
- package/dist/src/ui/contexts/ToolActionsContext.js.map +1 -1
- package/dist/src/ui/contexts/ToolActionsContext.test.js +90 -7
- package/dist/src/ui/contexts/ToolActionsContext.test.js.map +1 -1
- package/dist/src/ui/contexts/UIActionsContext.d.ts +4 -3
- package/dist/src/ui/contexts/UIActionsContext.js.map +1 -1
- package/dist/src/ui/contexts/UIStateContext.d.ts +10 -16
- package/dist/src/ui/contexts/UIStateContext.js.map +1 -1
- package/dist/src/ui/hooks/atCommandProcessor.test.js +2 -1
- package/dist/src/ui/hooks/atCommandProcessor.test.js.map +1 -1
- package/dist/src/ui/hooks/atCommandProcessor_agents.test.js +2 -1
- package/dist/src/ui/hooks/atCommandProcessor_agents.test.js.map +1 -1
- package/dist/src/ui/hooks/shellReducer.d.ts +12 -10
- package/dist/src/ui/hooks/shellReducer.js +67 -37
- package/dist/src/ui/hooks/shellReducer.js.map +1 -1
- package/dist/src/ui/hooks/shellReducer.test.js +207 -36
- package/dist/src/ui/hooks/shellReducer.test.js.map +1 -1
- package/dist/src/ui/hooks/slashCommandProcessor.d.ts +1 -1
- package/dist/src/ui/hooks/slashCommandProcessor.js +1 -1
- package/dist/src/ui/hooks/slashCommandProcessor.test.js +1 -1
- package/dist/src/ui/hooks/toolMapping.js +7 -0
- package/dist/src/ui/hooks/toolMapping.js.map +1 -1
- package/dist/src/ui/hooks/useAlternateBuffer.js +6 -1
- package/dist/src/ui/hooks/useAlternateBuffer.js.map +1 -1
- package/dist/src/ui/hooks/useAlternateBuffer.test.js +5 -0
- package/dist/src/ui/hooks/useAlternateBuffer.test.js.map +1 -1
- package/dist/src/ui/hooks/useAnimatedScrollbar.js +2 -2
- package/dist/src/ui/hooks/useAnimatedScrollbar.js.map +1 -1
- package/dist/src/ui/hooks/useAtCompletion.js +1 -1
- package/dist/src/ui/hooks/useAtCompletion.js.map +1 -1
- package/dist/src/ui/hooks/useBackgroundTaskManager.d.ts +22 -0
- package/dist/src/ui/hooks/useBackgroundTaskManager.js +58 -0
- package/dist/src/ui/hooks/useBackgroundTaskManager.js.map +1 -0
- package/dist/src/ui/hooks/{useBackgroundShellManager.test.js → useBackgroundTaskManager.test.js} +50 -50
- package/dist/src/ui/hooks/useBackgroundTaskManager.test.js.map +1 -0
- package/dist/src/ui/hooks/useBanner.d.ts +1 -0
- package/dist/src/ui/hooks/useBanner.js +16 -9
- package/dist/src/ui/hooks/useBanner.js.map +1 -1
- package/dist/src/ui/hooks/useBanner.test.js +7 -4
- package/dist/src/ui/hooks/useBanner.test.js.map +1 -1
- package/dist/src/ui/hooks/useBatchedScroll.js +2 -2
- package/dist/src/ui/hooks/useBatchedScroll.js.map +1 -1
- package/dist/src/ui/hooks/useCommandCompletion.d.ts +2 -1
- package/dist/src/ui/hooks/useCommandCompletion.js +13 -3
- package/dist/src/ui/hooks/useCommandCompletion.js.map +1 -1
- package/dist/src/ui/hooks/useCommandCompletion.test.d.ts +1 -1
- package/dist/src/ui/hooks/useCommandCompletion.test.js +82 -6
- package/dist/src/ui/hooks/useCommandCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useComposerStatus.d.ts +21 -0
- package/dist/src/ui/hooks/useComposerStatus.js +78 -0
- package/dist/src/ui/hooks/useComposerStatus.js.map +1 -0
- package/dist/src/ui/hooks/useConsoleMessages.test.js +2 -2
- package/dist/src/ui/hooks/useConsoleMessages.test.js.map +1 -1
- package/dist/src/ui/hooks/useExecutionLifecycle.d.ts +28 -0
- package/dist/src/ui/hooks/{shellCommandProcessor.js → useExecutionLifecycle.js} +140 -58
- package/dist/src/ui/hooks/useExecutionLifecycle.js.map +1 -0
- package/dist/src/ui/hooks/useExecutionLifecycle.test.d.ts +6 -0
- package/dist/src/ui/hooks/{shellCommandProcessor.test.js → useExecutionLifecycle.test.js} +123 -81
- package/dist/src/ui/hooks/useExecutionLifecycle.test.js.map +1 -0
- package/dist/src/ui/hooks/useFolderTrust.js +1 -1
- package/dist/src/ui/hooks/useFolderTrust.js.map +1 -1
- package/dist/src/ui/hooks/useFolderTrust.test.js +1 -1
- package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.d.ts +6 -6
- package/dist/src/ui/hooks/useGeminiStream.js +140 -38
- package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.test.js +176 -22
- package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -1
- package/dist/src/ui/hooks/useGitBranchName.js +2 -2
- package/dist/src/ui/hooks/useGitBranchName.js.map +1 -1
- package/dist/src/ui/hooks/useLoadingIndicator.js +1 -1
- package/dist/src/ui/hooks/useLoadingIndicator.js.map +1 -1
- package/dist/src/ui/hooks/useLoadingIndicator.test.js +10 -0
- package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
- package/dist/src/ui/hooks/usePermissionsModifyTrust.js +2 -2
- package/dist/src/ui/hooks/usePermissionsModifyTrust.js.map +1 -1
- package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js +1 -1
- package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js.map +1 -1
- package/dist/src/ui/hooks/usePhraseCycler.js +4 -2
- package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
- package/dist/src/ui/hooks/useSessionBrowser.d.ts +1 -1
- package/dist/src/ui/hooks/useSessionBrowser.js +2 -2
- package/dist/src/ui/hooks/useSessionBrowser.js.map +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.d.ts +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.js +37 -63
- package/dist/src/ui/hooks/useSlashCompletion.js.map +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.test.d.ts +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.test.js +57 -47
- package/dist/src/ui/hooks/useSlashCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useToolScheduler.d.ts +2 -1
- package/dist/src/ui/hooks/useToolScheduler.js +55 -1
- package/dist/src/ui/hooks/useToolScheduler.js.map +1 -1
- package/dist/src/ui/hooks/useToolScheduler.test.js +107 -9
- package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
- package/dist/src/ui/key/keyBindings.d.ts +9 -1
- package/dist/src/ui/key/keyBindings.js +40 -3
- package/dist/src/ui/key/keyBindings.js.map +1 -1
- package/dist/src/ui/key/keyMatchers.test.js +12 -2
- package/dist/src/ui/key/keyMatchers.test.js.map +1 -1
- package/dist/src/ui/layouts/DefaultAppLayout.js +8 -6
- package/dist/src/ui/layouts/DefaultAppLayout.js.map +1 -1
- package/dist/src/ui/layouts/DefaultAppLayout.test.js +27 -22
- package/dist/src/ui/layouts/DefaultAppLayout.test.js.map +1 -1
- package/dist/src/ui/noninteractive/nonInteractiveUi.js +1 -1
- package/dist/src/ui/themes/builtin/dark/tokyonight-dark.d.ts +7 -0
- package/dist/src/ui/themes/builtin/dark/tokyonight-dark.js +147 -0
- package/dist/src/ui/themes/builtin/dark/tokyonight-dark.js.map +1 -0
- package/dist/src/ui/themes/theme-manager.js +2 -0
- package/dist/src/ui/themes/theme-manager.js.map +1 -1
- package/dist/src/ui/themes/theme.js +1 -1
- package/dist/src/ui/themes/theme.js.map +1 -1
- package/dist/src/ui/types.d.ts +10 -2
- package/dist/src/ui/types.js.map +1 -1
- package/dist/src/ui/utils/CodeColorizer.d.ts +1 -0
- package/dist/src/ui/utils/CodeColorizer.js +17 -18
- package/dist/src/ui/utils/CodeColorizer.js.map +1 -1
- package/dist/src/ui/utils/ConsolePatcher.d.ts +1 -0
- package/dist/src/ui/utils/ConsolePatcher.js +12 -5
- package/dist/src/ui/utils/ConsolePatcher.js.map +1 -1
- package/dist/src/ui/utils/ConsolePatcher.test.d.ts +6 -0
- package/dist/src/ui/utils/ConsolePatcher.test.js +199 -0
- package/dist/src/ui/utils/ConsolePatcher.test.js.map +1 -0
- package/dist/src/ui/utils/TableRenderer.js +3 -3
- package/dist/src/ui/utils/TableRenderer.js.map +1 -1
- package/dist/src/ui/utils/borderStyles.d.ts +2 -2
- package/dist/src/ui/utils/borderStyles.js +2 -2
- package/dist/src/ui/utils/borderStyles.js.map +1 -1
- package/dist/src/ui/utils/directoryUtils.js +1 -1
- package/dist/src/ui/utils/directoryUtils.js.map +1 -1
- package/dist/src/ui/utils/fileUtils.d.ts +10 -0
- package/dist/src/ui/utils/fileUtils.js +17 -0
- package/dist/src/ui/utils/fileUtils.js.map +1 -0
- package/dist/src/ui/utils/terminalCapabilityManager.d.ts +1 -0
- package/dist/src/ui/utils/terminalCapabilityManager.js +8 -0
- package/dist/src/ui/utils/terminalCapabilityManager.js.map +1 -1
- package/dist/src/ui/utils/terminalCapabilityManager.test.js +38 -0
- package/dist/src/ui/utils/terminalCapabilityManager.test.js.map +1 -1
- package/dist/src/ui/utils/toolLayoutUtils.d.ts +1 -1
- package/dist/src/ui/utils/toolLayoutUtils.js +1 -1
- package/dist/src/ui/utils/ui-sizing.test.js +1 -0
- package/dist/src/ui/utils/ui-sizing.test.js.map +1 -1
- package/dist/src/ui/utils/updateCheck.d.ts +1 -0
- package/dist/src/ui/utils/updateCheck.js.map +1 -1
- package/dist/src/utils/activityLogger.js +16 -0
- package/dist/src/utils/activityLogger.js.map +1 -1
- package/dist/src/utils/cleanup.js +12 -5
- package/dist/src/utils/cleanup.js.map +1 -1
- package/dist/src/utils/commands.js +15 -0
- package/dist/src/utils/commands.js.map +1 -1
- package/dist/src/utils/commands.test.js +79 -0
- package/dist/src/utils/commands.test.js.map +1 -1
- package/dist/src/utils/envVarResolver.d.ts +5 -2
- package/dist/src/utils/envVarResolver.js +15 -6
- package/dist/src/utils/envVarResolver.js.map +1 -1
- package/dist/src/utils/envVarResolver.test.js +41 -24
- package/dist/src/utils/envVarResolver.test.js.map +1 -1
- package/dist/src/utils/errors.js +4 -4
- package/dist/src/utils/errors.js.map +1 -1
- package/dist/src/utils/events.d.ts +3 -1
- package/dist/src/utils/events.js +1 -0
- package/dist/src/utils/events.js.map +1 -1
- package/dist/src/utils/gitUtils.js +4 -4
- package/dist/src/utils/gitUtils.js.map +1 -1
- package/dist/src/utils/handleAutoUpdate.js +10 -3
- package/dist/src/utils/handleAutoUpdate.js.map +1 -1
- package/dist/src/utils/handleAutoUpdate.test.js +8 -2
- package/dist/src/utils/handleAutoUpdate.test.js.map +1 -1
- package/dist/src/utils/installationInfo.js +1 -1
- package/dist/src/utils/installationInfo.js.map +1 -1
- package/dist/src/utils/jsonoutput.js +1 -1
- package/dist/src/utils/jsonoutput.js.map +1 -1
- package/dist/src/utils/sandboxUtils.js +1 -1
- package/dist/src/utils/sandboxUtils.js.map +1 -1
- package/dist/src/utils/sessionCleanup.js +7 -30
- package/dist/src/utils/sessionCleanup.js.map +1 -1
- package/dist/src/utils/sessionCleanup.test.js +3 -0
- package/dist/src/utils/sessionCleanup.test.js.map +1 -1
- package/dist/src/utils/sessionUtils.js +1 -0
- package/dist/src/utils/sessionUtils.js.map +1 -1
- package/dist/src/utils/sessionUtils.test.js +179 -3
- package/dist/src/utils/sessionUtils.test.js.map +1 -1
- package/dist/src/utils/sessions.js +1 -1
- package/dist/src/utils/sessions.js.map +1 -1
- package/dist/src/utils/skillUtils.js +3 -1
- package/dist/src/utils/skillUtils.js.map +1 -1
- package/dist/src/utils/skillUtils.test.js +4 -6
- package/dist/src/utils/skillUtils.test.js.map +1 -1
- package/dist/src/utils/terminalNotifications.js +2 -4
- package/dist/src/utils/terminalNotifications.js.map +1 -1
- package/dist/src/utils/terminalNotifications.test.js +5 -3
- package/dist/src/utils/terminalNotifications.test.js.map +1 -1
- package/dist/src/utils/userStartupWarnings.js +2 -2
- package/dist/src/utils/userStartupWarnings.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/dist/src/ui/commands/shellsCommand.js.map +0 -1
- package/dist/src/ui/commands/shellsCommand.test.js +0 -29
- package/dist/src/ui/commands/shellsCommand.test.js.map +0 -1
- package/dist/src/ui/components/BackgroundShellDisplay.d.ts +0 -16
- package/dist/src/ui/components/BackgroundShellDisplay.js.map +0 -1
- package/dist/src/ui/components/BackgroundShellDisplay.test.js.map +0 -1
- package/dist/src/ui/hooks/shellCommandProcessor.d.ts +0 -28
- package/dist/src/ui/hooks/shellCommandProcessor.js.map +0 -1
- package/dist/src/ui/hooks/shellCommandProcessor.test.js.map +0 -1
- package/dist/src/ui/hooks/useBackgroundShellManager.d.ts +0 -22
- package/dist/src/ui/hooks/useBackgroundShellManager.js +0 -58
- package/dist/src/ui/hooks/useBackgroundShellManager.js.map +0 -1
- package/dist/src/ui/hooks/useBackgroundShellManager.test.js.map +0 -1
- /package/dist/src/{ui/commands/shellsCommand.test.d.ts → nonInteractiveCliAgentSession.test.d.ts} +0 -0
- /package/dist/src/ui/{components/BackgroundShellDisplay.test.d.ts → commands/tasksCommand.test.d.ts} +0 -0
- /package/dist/src/ui/{hooks/shellCommandProcessor.test.d.ts → components/BackgroundTaskDisplay.test.d.ts} +0 -0
- /package/dist/src/ui/hooks/{useBackgroundShellManager.test.d.ts → useBackgroundTaskManager.test.d.ts} +0 -0
|
@@ -0,0 +1,1837 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { ToolErrorType, GeminiEventType, OutputFormat, uiTelemetryService, FatalInputError, CoreEvent, CoreToolCallStatus, } from '@google/gemini-cli-core';
|
|
7
|
+
import { runNonInteractive } from './nonInteractiveCliAgentSession.js';
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach, vi, } from 'vitest';
|
|
9
|
+
// Mock core modules
|
|
10
|
+
vi.mock('./ui/hooks/atCommandProcessor.js');
|
|
11
|
+
const mockSetupInitialActivityLogger = vi.hoisted(() => vi.fn());
|
|
12
|
+
vi.mock('./utils/devtoolsService.js', () => ({
|
|
13
|
+
setupInitialActivityLogger: mockSetupInitialActivityLogger,
|
|
14
|
+
}));
|
|
15
|
+
const mockCoreEvents = vi.hoisted(() => ({
|
|
16
|
+
on: vi.fn(),
|
|
17
|
+
off: vi.fn(),
|
|
18
|
+
emit: vi.fn(),
|
|
19
|
+
emitConsoleLog: vi.fn(),
|
|
20
|
+
emitFeedback: vi.fn(),
|
|
21
|
+
drainBacklogs: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
const mockSchedulerSchedule = vi.hoisted(() => vi.fn());
|
|
24
|
+
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|
25
|
+
const original = await importOriginal();
|
|
26
|
+
class MockChatRecordingService {
|
|
27
|
+
initialize = vi.fn();
|
|
28
|
+
recordMessage = vi.fn();
|
|
29
|
+
recordMessageTokens = vi.fn();
|
|
30
|
+
recordToolCalls = vi.fn();
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
...original,
|
|
34
|
+
Scheduler: class {
|
|
35
|
+
schedule = mockSchedulerSchedule;
|
|
36
|
+
cancelAll = vi.fn();
|
|
37
|
+
dispose = vi.fn();
|
|
38
|
+
},
|
|
39
|
+
isTelemetrySdkInitialized: vi.fn().mockReturnValue(true),
|
|
40
|
+
ChatRecordingService: MockChatRecordingService,
|
|
41
|
+
uiTelemetryService: {
|
|
42
|
+
getMetrics: vi.fn(),
|
|
43
|
+
},
|
|
44
|
+
LegacyAgentSession: original.LegacyAgentSession,
|
|
45
|
+
geminiPartsToContentParts: original.geminiPartsToContentParts,
|
|
46
|
+
coreEvents: mockCoreEvents,
|
|
47
|
+
createWorkingStdio: vi.fn(() => ({
|
|
48
|
+
stdout: process.stdout,
|
|
49
|
+
stderr: process.stderr,
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
const mockGetCommands = vi.hoisted(() => vi.fn());
|
|
54
|
+
const mockCommandServiceCreate = vi.hoisted(() => vi.fn());
|
|
55
|
+
vi.mock('./services/CommandService.js', () => ({
|
|
56
|
+
CommandService: {
|
|
57
|
+
create: mockCommandServiceCreate,
|
|
58
|
+
},
|
|
59
|
+
}));
|
|
60
|
+
vi.mock('./services/FileCommandLoader.js');
|
|
61
|
+
vi.mock('./services/McpPromptLoader.js');
|
|
62
|
+
vi.mock('./services/BuiltinCommandLoader.js');
|
|
63
|
+
describe('runNonInteractive', () => {
|
|
64
|
+
let mockConfig;
|
|
65
|
+
let mockSettings;
|
|
66
|
+
let mockToolRegistry;
|
|
67
|
+
let consoleErrorSpy;
|
|
68
|
+
let processStdoutSpy;
|
|
69
|
+
let processStderrSpy;
|
|
70
|
+
let mockGeminiClient;
|
|
71
|
+
const MOCK_SESSION_METRICS = {
|
|
72
|
+
models: {},
|
|
73
|
+
tools: {
|
|
74
|
+
totalCalls: 0,
|
|
75
|
+
totalSuccess: 0,
|
|
76
|
+
totalFail: 0,
|
|
77
|
+
totalDurationMs: 0,
|
|
78
|
+
totalDecisions: {
|
|
79
|
+
accept: 0,
|
|
80
|
+
reject: 0,
|
|
81
|
+
modify: 0,
|
|
82
|
+
auto_accept: 0,
|
|
83
|
+
},
|
|
84
|
+
byName: {},
|
|
85
|
+
},
|
|
86
|
+
files: {
|
|
87
|
+
totalLinesAdded: 0,
|
|
88
|
+
totalLinesRemoved: 0,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
beforeEach(async () => {
|
|
92
|
+
mockSchedulerSchedule.mockReset();
|
|
93
|
+
mockCommandServiceCreate.mockResolvedValue({
|
|
94
|
+
getCommands: mockGetCommands,
|
|
95
|
+
});
|
|
96
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
97
|
+
processStdoutSpy = vi
|
|
98
|
+
.spyOn(process.stdout, 'write')
|
|
99
|
+
.mockImplementation(() => true);
|
|
100
|
+
vi.spyOn(process.stdout, 'on').mockImplementation(() => process.stdout);
|
|
101
|
+
processStderrSpy = vi
|
|
102
|
+
.spyOn(process.stderr, 'write')
|
|
103
|
+
.mockImplementation(() => true);
|
|
104
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
105
|
+
throw new Error(`process.exit(${code}) called`);
|
|
106
|
+
});
|
|
107
|
+
mockToolRegistry = {
|
|
108
|
+
getTool: vi.fn(),
|
|
109
|
+
getFunctionDeclarations: vi.fn().mockReturnValue([]),
|
|
110
|
+
};
|
|
111
|
+
mockGeminiClient = {
|
|
112
|
+
sendMessageStream: vi.fn(),
|
|
113
|
+
resumeChat: vi.fn().mockResolvedValue(undefined),
|
|
114
|
+
getChatRecordingService: vi.fn(() => ({
|
|
115
|
+
initialize: vi.fn(),
|
|
116
|
+
recordMessage: vi.fn(),
|
|
117
|
+
recordMessageTokens: vi.fn(),
|
|
118
|
+
recordToolCalls: vi.fn(),
|
|
119
|
+
})),
|
|
120
|
+
getChat: vi.fn(() => ({ recordCompletedToolCalls: vi.fn() })),
|
|
121
|
+
getCurrentSequenceModel: vi.fn().mockReturnValue(null),
|
|
122
|
+
};
|
|
123
|
+
mockConfig = {
|
|
124
|
+
initialize: vi.fn().mockReturnValue(Promise.resolve(undefined)),
|
|
125
|
+
getMessageBus: vi.fn().mockReturnValue({
|
|
126
|
+
subscribe: vi.fn(),
|
|
127
|
+
unsubscribe: vi.fn(),
|
|
128
|
+
publish: vi.fn(),
|
|
129
|
+
}),
|
|
130
|
+
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
|
|
131
|
+
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
|
|
132
|
+
getMaxSessionTurns: vi.fn().mockReturnValue(10),
|
|
133
|
+
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
|
134
|
+
getProjectRoot: vi.fn().mockReturnValue('/test/project'),
|
|
135
|
+
storage: {
|
|
136
|
+
getProjectTempDir: vi
|
|
137
|
+
.fn()
|
|
138
|
+
.mockReturnValue('/test/project/.cell-cli/tmp'),
|
|
139
|
+
},
|
|
140
|
+
getIdeMode: vi.fn().mockReturnValue(false),
|
|
141
|
+
getContentGeneratorConfig: vi.fn().mockReturnValue({}),
|
|
142
|
+
getDebugMode: vi.fn().mockReturnValue(false),
|
|
143
|
+
getOutputFormat: vi.fn().mockReturnValue('text'),
|
|
144
|
+
getModel: vi.fn().mockReturnValue('test-model'),
|
|
145
|
+
getFolderTrust: vi.fn().mockReturnValue(false),
|
|
146
|
+
isTrustedFolder: vi.fn().mockReturnValue(false),
|
|
147
|
+
getRawOutput: vi.fn().mockReturnValue(false),
|
|
148
|
+
getAcceptRawOutputRisk: vi.fn().mockReturnValue(false),
|
|
149
|
+
getAgentSessionNoninteractiveEnabled: vi.fn().mockReturnValue(false),
|
|
150
|
+
};
|
|
151
|
+
mockSettings = {
|
|
152
|
+
system: { path: '', settings: {} },
|
|
153
|
+
systemDefaults: { path: '', settings: {} },
|
|
154
|
+
user: { path: '', settings: {} },
|
|
155
|
+
workspace: { path: '', settings: {} },
|
|
156
|
+
errors: [],
|
|
157
|
+
setValue: vi.fn(),
|
|
158
|
+
merged: {
|
|
159
|
+
security: {
|
|
160
|
+
auth: {
|
|
161
|
+
enforcedType: undefined,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
isTrusted: true,
|
|
166
|
+
migratedInMemoryScopes: new Set(),
|
|
167
|
+
forScope: vi.fn(),
|
|
168
|
+
computeMergedSettings: vi.fn(),
|
|
169
|
+
};
|
|
170
|
+
const { handleAtCommand } = await import('./ui/hooks/atCommandProcessor.js');
|
|
171
|
+
vi.mocked(handleAtCommand).mockImplementation(async ({ query }) => ({
|
|
172
|
+
processedQuery: [{ text: query }],
|
|
173
|
+
}));
|
|
174
|
+
});
|
|
175
|
+
afterEach(() => {
|
|
176
|
+
vi.restoreAllMocks();
|
|
177
|
+
});
|
|
178
|
+
async function* createStreamFromEvents(events) {
|
|
179
|
+
for (const event of events) {
|
|
180
|
+
yield event;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const getWrittenOutput = () => processStdoutSpy.mock.calls.map((c) => c[0]).join('');
|
|
184
|
+
it('should process input and write text output', async () => {
|
|
185
|
+
const events = [
|
|
186
|
+
{ type: GeminiEventType.Content, value: 'Hello' },
|
|
187
|
+
{ type: GeminiEventType.Content, value: ' World' },
|
|
188
|
+
{
|
|
189
|
+
type: GeminiEventType.Finished,
|
|
190
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
194
|
+
await runNonInteractive({
|
|
195
|
+
config: mockConfig,
|
|
196
|
+
settings: mockSettings,
|
|
197
|
+
input: 'Test input',
|
|
198
|
+
prompt_id: 'prompt-id-1',
|
|
199
|
+
});
|
|
200
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith([{ text: 'Test input' }], expect.any(AbortSignal), 'prompt-id-1', undefined, false, 'Test input');
|
|
201
|
+
expect(getWrittenOutput()).toBe('Hello World\n');
|
|
202
|
+
// Note: Telemetry shutdown is now handled in runExitCleanup() in cleanup.ts
|
|
203
|
+
// so we no longer expect shutdownTelemetry to be called directly here
|
|
204
|
+
});
|
|
205
|
+
it('should stream the specific stream started by send', async () => {
|
|
206
|
+
const { LegacyAgentSession } = await import('@google/gemini-cli-core');
|
|
207
|
+
const streamSpy = vi.spyOn(LegacyAgentSession.prototype, 'stream');
|
|
208
|
+
const events = [
|
|
209
|
+
{ type: GeminiEventType.Content, value: 'Hello again' },
|
|
210
|
+
{
|
|
211
|
+
type: GeminiEventType.Finished,
|
|
212
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
216
|
+
await runNonInteractive({
|
|
217
|
+
config: mockConfig,
|
|
218
|
+
settings: mockSettings,
|
|
219
|
+
input: 'Test input',
|
|
220
|
+
prompt_id: 'prompt-id-stream',
|
|
221
|
+
});
|
|
222
|
+
expect(streamSpy).toHaveBeenCalledWith({ streamId: expect.any(String) });
|
|
223
|
+
});
|
|
224
|
+
it('fails fast if the session acknowledges a message send without a stream', async () => {
|
|
225
|
+
const { LegacyAgentSession } = await import('@google/gemini-cli-core');
|
|
226
|
+
const sendSpy = vi
|
|
227
|
+
.spyOn(LegacyAgentSession.prototype, 'send')
|
|
228
|
+
.mockResolvedValue({ streamId: null });
|
|
229
|
+
const streamSpy = vi.spyOn(LegacyAgentSession.prototype, 'stream');
|
|
230
|
+
await expect(runNonInteractive({
|
|
231
|
+
config: mockConfig,
|
|
232
|
+
settings: mockSettings,
|
|
233
|
+
input: 'Test input',
|
|
234
|
+
prompt_id: 'prompt-id-null-stream',
|
|
235
|
+
})).rejects.toThrow('LegacyAgentSession.send() unexpectedly returned no stream for a message send.');
|
|
236
|
+
expect(streamSpy).not.toHaveBeenCalled();
|
|
237
|
+
sendSpy.mockRestore();
|
|
238
|
+
streamSpy.mockRestore();
|
|
239
|
+
});
|
|
240
|
+
it('should register activity logger when CELL_CLI_ACTIVITY_LOG_TARGET is set', async () => {
|
|
241
|
+
vi.stubEnv('CELL_CLI_ACTIVITY_LOG_TARGET', '/tmp/test.jsonl');
|
|
242
|
+
const events = [
|
|
243
|
+
{
|
|
244
|
+
type: GeminiEventType.Finished,
|
|
245
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
249
|
+
await runNonInteractive({
|
|
250
|
+
config: mockConfig,
|
|
251
|
+
settings: mockSettings,
|
|
252
|
+
input: 'test',
|
|
253
|
+
prompt_id: 'prompt-id-activity-logger',
|
|
254
|
+
});
|
|
255
|
+
expect(mockSetupInitialActivityLogger).toHaveBeenCalledWith(mockConfig);
|
|
256
|
+
vi.unstubAllEnvs();
|
|
257
|
+
});
|
|
258
|
+
it('should not register activity logger when CELL_CLI_ACTIVITY_LOG_TARGET is not set', async () => {
|
|
259
|
+
vi.stubEnv('CELL_CLI_ACTIVITY_LOG_TARGET', '');
|
|
260
|
+
const events = [
|
|
261
|
+
{
|
|
262
|
+
type: GeminiEventType.Finished,
|
|
263
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
267
|
+
await runNonInteractive({
|
|
268
|
+
config: mockConfig,
|
|
269
|
+
settings: mockSettings,
|
|
270
|
+
input: 'test',
|
|
271
|
+
prompt_id: 'prompt-id-activity-logger-off',
|
|
272
|
+
});
|
|
273
|
+
expect(mockSetupInitialActivityLogger).not.toHaveBeenCalled();
|
|
274
|
+
vi.unstubAllEnvs();
|
|
275
|
+
});
|
|
276
|
+
it('should handle a single tool call and respond', async () => {
|
|
277
|
+
const toolCallEvent = {
|
|
278
|
+
type: GeminiEventType.ToolCallRequest,
|
|
279
|
+
value: {
|
|
280
|
+
callId: 'tool-1',
|
|
281
|
+
name: 'testTool',
|
|
282
|
+
args: { arg1: 'value1' },
|
|
283
|
+
isClientInitiated: false,
|
|
284
|
+
prompt_id: 'prompt-id-2',
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
const toolResponse = [{ text: 'Tool response' }];
|
|
288
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
289
|
+
{
|
|
290
|
+
status: CoreToolCallStatus.Success,
|
|
291
|
+
request: {
|
|
292
|
+
callId: 'tool-1',
|
|
293
|
+
name: 'testTool',
|
|
294
|
+
args: { arg1: 'value1' },
|
|
295
|
+
isClientInitiated: false,
|
|
296
|
+
prompt_id: 'prompt-id-2',
|
|
297
|
+
},
|
|
298
|
+
tool: {},
|
|
299
|
+
invocation: {},
|
|
300
|
+
response: {
|
|
301
|
+
responseParts: toolResponse,
|
|
302
|
+
callId: 'tool-1',
|
|
303
|
+
error: undefined,
|
|
304
|
+
errorType: undefined,
|
|
305
|
+
contentLength: undefined,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
]);
|
|
309
|
+
const firstCallEvents = [toolCallEvent];
|
|
310
|
+
const secondCallEvents = [
|
|
311
|
+
{ type: GeminiEventType.Content, value: 'Final answer' },
|
|
312
|
+
{
|
|
313
|
+
type: GeminiEventType.Finished,
|
|
314
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
mockGeminiClient.sendMessageStream
|
|
318
|
+
.mockReturnValueOnce(createStreamFromEvents(firstCallEvents))
|
|
319
|
+
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
|
|
320
|
+
await runNonInteractive({
|
|
321
|
+
config: mockConfig,
|
|
322
|
+
settings: mockSettings,
|
|
323
|
+
input: 'Use a tool',
|
|
324
|
+
prompt_id: 'prompt-id-2',
|
|
325
|
+
});
|
|
326
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
|
|
327
|
+
expect(mockSchedulerSchedule).toHaveBeenCalledWith([expect.objectContaining({ name: 'testTool' })], expect.any(AbortSignal));
|
|
328
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith(2, [{ text: 'Tool response' }], expect.any(AbortSignal), 'prompt-id-2', undefined, false, undefined);
|
|
329
|
+
expect(getWrittenOutput()).toBe('Final answer\n');
|
|
330
|
+
});
|
|
331
|
+
it('should write a single newline between sequential text outputs from the model', async () => {
|
|
332
|
+
// This test simulates a multi-turn conversation to ensure that a single newline
|
|
333
|
+
// is printed between each block of text output from the model.
|
|
334
|
+
// 1. Define the tool requests that the model will ask the CLI to run.
|
|
335
|
+
const toolCallEvent = {
|
|
336
|
+
type: GeminiEventType.ToolCallRequest,
|
|
337
|
+
value: {
|
|
338
|
+
callId: 'mock-tool',
|
|
339
|
+
name: 'mockTool',
|
|
340
|
+
args: {},
|
|
341
|
+
isClientInitiated: false,
|
|
342
|
+
prompt_id: 'prompt-id-multi',
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
// 2. Mock the execution of the tools. We just need them to succeed.
|
|
346
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
347
|
+
{
|
|
348
|
+
status: CoreToolCallStatus.Success,
|
|
349
|
+
request: toolCallEvent.value, // This is generic enough for both calls
|
|
350
|
+
tool: {},
|
|
351
|
+
invocation: {},
|
|
352
|
+
response: {
|
|
353
|
+
responseParts: [],
|
|
354
|
+
callId: 'mock-tool',
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
]);
|
|
358
|
+
// 3. Define the sequence of events streamed from the mock model.
|
|
359
|
+
// Turn 1: Model outputs text, then requests a tool call.
|
|
360
|
+
const modelTurn1 = [
|
|
361
|
+
{ type: GeminiEventType.Content, value: 'Use mock tool' },
|
|
362
|
+
toolCallEvent,
|
|
363
|
+
];
|
|
364
|
+
// Turn 2: Model outputs more text, then requests another tool call.
|
|
365
|
+
const modelTurn2 = [
|
|
366
|
+
{ type: GeminiEventType.Content, value: 'Use mock tool again' },
|
|
367
|
+
toolCallEvent,
|
|
368
|
+
];
|
|
369
|
+
// Turn 3: Model outputs a final answer.
|
|
370
|
+
const modelTurn3 = [
|
|
371
|
+
{ type: GeminiEventType.Content, value: 'Finished.' },
|
|
372
|
+
{
|
|
373
|
+
type: GeminiEventType.Finished,
|
|
374
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
375
|
+
},
|
|
376
|
+
];
|
|
377
|
+
mockGeminiClient.sendMessageStream
|
|
378
|
+
.mockReturnValueOnce(createStreamFromEvents(modelTurn1))
|
|
379
|
+
.mockReturnValueOnce(createStreamFromEvents(modelTurn2))
|
|
380
|
+
.mockReturnValueOnce(createStreamFromEvents(modelTurn3));
|
|
381
|
+
// 4. Run the command.
|
|
382
|
+
await runNonInteractive({
|
|
383
|
+
config: mockConfig,
|
|
384
|
+
settings: mockSettings,
|
|
385
|
+
input: 'Use mock tool multiple times',
|
|
386
|
+
prompt_id: 'prompt-id-multi',
|
|
387
|
+
});
|
|
388
|
+
// 5. Verify the output.
|
|
389
|
+
// The rendered output should contain the text from each turn, separated by a
|
|
390
|
+
// single newline, with a final newline at the end.
|
|
391
|
+
expect(getWrittenOutput()).toMatchSnapshot();
|
|
392
|
+
// Also verify the tools were called as expected.
|
|
393
|
+
expect(mockSchedulerSchedule).toHaveBeenCalledTimes(2);
|
|
394
|
+
});
|
|
395
|
+
it('should handle error during tool execution and should send error back to the model', async () => {
|
|
396
|
+
const toolCallEvent = {
|
|
397
|
+
type: GeminiEventType.ToolCallRequest,
|
|
398
|
+
value: {
|
|
399
|
+
callId: 'tool-1',
|
|
400
|
+
name: 'errorTool',
|
|
401
|
+
args: {},
|
|
402
|
+
isClientInitiated: false,
|
|
403
|
+
prompt_id: 'prompt-id-3',
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
407
|
+
{
|
|
408
|
+
status: CoreToolCallStatus.Error,
|
|
409
|
+
request: {
|
|
410
|
+
callId: 'tool-1',
|
|
411
|
+
name: 'errorTool',
|
|
412
|
+
args: {},
|
|
413
|
+
isClientInitiated: false,
|
|
414
|
+
prompt_id: 'prompt-id-3',
|
|
415
|
+
},
|
|
416
|
+
tool: {},
|
|
417
|
+
response: {
|
|
418
|
+
callId: 'tool-1',
|
|
419
|
+
error: new Error('Execution failed'),
|
|
420
|
+
errorType: ToolErrorType.EXECUTION_FAILED,
|
|
421
|
+
responseParts: [
|
|
422
|
+
{
|
|
423
|
+
functionResponse: {
|
|
424
|
+
name: 'errorTool',
|
|
425
|
+
response: {
|
|
426
|
+
output: 'Error: Execution failed',
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
resultDisplay: 'Execution failed',
|
|
432
|
+
contentLength: undefined,
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
]);
|
|
436
|
+
const finalResponse = [
|
|
437
|
+
{
|
|
438
|
+
type: GeminiEventType.Content,
|
|
439
|
+
value: 'Sorry, let me try again.',
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
type: GeminiEventType.Finished,
|
|
443
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
mockGeminiClient.sendMessageStream
|
|
447
|
+
.mockReturnValueOnce(createStreamFromEvents([toolCallEvent]))
|
|
448
|
+
.mockReturnValueOnce(createStreamFromEvents(finalResponse));
|
|
449
|
+
await runNonInteractive({
|
|
450
|
+
config: mockConfig,
|
|
451
|
+
settings: mockSettings,
|
|
452
|
+
input: 'Trigger tool error',
|
|
453
|
+
prompt_id: 'prompt-id-3',
|
|
454
|
+
});
|
|
455
|
+
expect(mockSchedulerSchedule).toHaveBeenCalled();
|
|
456
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error executing tool errorTool: Execution failed');
|
|
457
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
|
|
458
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith(2, [
|
|
459
|
+
{
|
|
460
|
+
functionResponse: {
|
|
461
|
+
name: 'errorTool',
|
|
462
|
+
response: {
|
|
463
|
+
output: 'Error: Execution failed',
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
], expect.any(AbortSignal), 'prompt-id-3', undefined, false, undefined);
|
|
468
|
+
expect(getWrittenOutput()).toBe('Sorry, let me try again.\n');
|
|
469
|
+
});
|
|
470
|
+
it('should exit with error if sendMessageStream throws initially', async () => {
|
|
471
|
+
const apiError = new Error('API connection failed');
|
|
472
|
+
mockGeminiClient.sendMessageStream.mockImplementation(() => {
|
|
473
|
+
throw apiError;
|
|
474
|
+
});
|
|
475
|
+
await expect(runNonInteractive({
|
|
476
|
+
config: mockConfig,
|
|
477
|
+
settings: mockSettings,
|
|
478
|
+
input: 'Initial fail',
|
|
479
|
+
prompt_id: 'prompt-id-4',
|
|
480
|
+
})).rejects.toThrow('API connection failed');
|
|
481
|
+
});
|
|
482
|
+
it('should not exit if a tool is not found, and should send error back to model', async () => {
|
|
483
|
+
const toolCallEvent = {
|
|
484
|
+
type: GeminiEventType.ToolCallRequest,
|
|
485
|
+
value: {
|
|
486
|
+
callId: 'tool-1',
|
|
487
|
+
name: 'nonexistentTool',
|
|
488
|
+
args: {},
|
|
489
|
+
isClientInitiated: false,
|
|
490
|
+
prompt_id: 'prompt-id-5',
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
494
|
+
{
|
|
495
|
+
status: CoreToolCallStatus.Error,
|
|
496
|
+
request: {
|
|
497
|
+
callId: 'tool-1',
|
|
498
|
+
name: 'nonexistentTool',
|
|
499
|
+
args: {},
|
|
500
|
+
isClientInitiated: false,
|
|
501
|
+
prompt_id: 'prompt-id-5',
|
|
502
|
+
},
|
|
503
|
+
response: {
|
|
504
|
+
callId: 'tool-1',
|
|
505
|
+
error: new Error('Tool "nonexistentTool" not found in registry.'),
|
|
506
|
+
resultDisplay: 'Tool "nonexistentTool" not found in registry.',
|
|
507
|
+
responseParts: [],
|
|
508
|
+
errorType: undefined,
|
|
509
|
+
contentLength: undefined,
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
]);
|
|
513
|
+
const finalResponse = [
|
|
514
|
+
{
|
|
515
|
+
type: GeminiEventType.Content,
|
|
516
|
+
value: "Sorry, I can't find that tool.",
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
type: GeminiEventType.Finished,
|
|
520
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
521
|
+
},
|
|
522
|
+
];
|
|
523
|
+
mockGeminiClient.sendMessageStream
|
|
524
|
+
.mockReturnValueOnce(createStreamFromEvents([toolCallEvent]))
|
|
525
|
+
.mockReturnValueOnce(createStreamFromEvents(finalResponse));
|
|
526
|
+
await runNonInteractive({
|
|
527
|
+
config: mockConfig,
|
|
528
|
+
settings: mockSettings,
|
|
529
|
+
input: 'Trigger tool not found',
|
|
530
|
+
prompt_id: 'prompt-id-5',
|
|
531
|
+
});
|
|
532
|
+
expect(mockSchedulerSchedule).toHaveBeenCalled();
|
|
533
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error executing tool nonexistentTool: Tool "nonexistentTool" not found in registry.');
|
|
534
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
|
|
535
|
+
expect(getWrittenOutput()).toBe("Sorry, I can't find that tool.\n");
|
|
536
|
+
});
|
|
537
|
+
it('should exit when max session turns are exceeded', async () => {
|
|
538
|
+
vi.mocked(mockConfig.getMaxSessionTurns).mockReturnValue(0);
|
|
539
|
+
await expect(runNonInteractive({
|
|
540
|
+
config: mockConfig,
|
|
541
|
+
settings: mockSettings,
|
|
542
|
+
input: 'Trigger loop',
|
|
543
|
+
prompt_id: 'prompt-id-6',
|
|
544
|
+
})).rejects.toThrow('Reached max session turns for this session');
|
|
545
|
+
});
|
|
546
|
+
it('should preprocess @include commands before sending to the model', async () => {
|
|
547
|
+
// 1. Mock the imported atCommandProcessor
|
|
548
|
+
const { handleAtCommand } = await import('./ui/hooks/atCommandProcessor.js');
|
|
549
|
+
const mockHandleAtCommand = vi.mocked(handleAtCommand);
|
|
550
|
+
// 2. Define the raw input and the expected processed output
|
|
551
|
+
const rawInput = 'Summarize @file.txt';
|
|
552
|
+
const processedParts = [
|
|
553
|
+
{ text: 'Summarize @file.txt' },
|
|
554
|
+
{ text: '\n--- Content from referenced files ---\n' },
|
|
555
|
+
{ text: 'This is the content of the file.' },
|
|
556
|
+
{ text: '\n--- End of content ---' },
|
|
557
|
+
];
|
|
558
|
+
// 3. Setup the mock to return the processed parts
|
|
559
|
+
mockHandleAtCommand.mockResolvedValue({
|
|
560
|
+
processedQuery: processedParts,
|
|
561
|
+
});
|
|
562
|
+
// Mock a simple stream response from the Gemini client
|
|
563
|
+
const events = [
|
|
564
|
+
{ type: GeminiEventType.Content, value: 'Summary complete.' },
|
|
565
|
+
{
|
|
566
|
+
type: GeminiEventType.Finished,
|
|
567
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
568
|
+
},
|
|
569
|
+
];
|
|
570
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
571
|
+
// 4. Run the non-interactive mode with the raw input
|
|
572
|
+
await runNonInteractive({
|
|
573
|
+
config: mockConfig,
|
|
574
|
+
settings: mockSettings,
|
|
575
|
+
input: rawInput,
|
|
576
|
+
prompt_id: 'prompt-id-7',
|
|
577
|
+
});
|
|
578
|
+
// 5. Assert that sendMessageStream was called with the PROCESSED parts, not the raw input
|
|
579
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith(processedParts, expect.any(AbortSignal), 'prompt-id-7', undefined, false, rawInput);
|
|
580
|
+
// 6. Assert the final output is correct
|
|
581
|
+
expect(getWrittenOutput()).toBe('Summary complete.\n');
|
|
582
|
+
});
|
|
583
|
+
it('should process input and write JSON output with stats', async () => {
|
|
584
|
+
const events = [
|
|
585
|
+
{ type: GeminiEventType.Content, value: 'Hello World' },
|
|
586
|
+
{
|
|
587
|
+
type: GeminiEventType.Finished,
|
|
588
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
589
|
+
},
|
|
590
|
+
];
|
|
591
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
592
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON);
|
|
593
|
+
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(MOCK_SESSION_METRICS);
|
|
594
|
+
await runNonInteractive({
|
|
595
|
+
config: mockConfig,
|
|
596
|
+
settings: mockSettings,
|
|
597
|
+
input: 'Test input',
|
|
598
|
+
prompt_id: 'prompt-id-1',
|
|
599
|
+
});
|
|
600
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith([{ text: 'Test input' }], expect.any(AbortSignal), 'prompt-id-1', undefined, false, 'Test input');
|
|
601
|
+
expect(processStdoutSpy).toHaveBeenCalledWith(JSON.stringify({
|
|
602
|
+
session_id: 'test-session-id',
|
|
603
|
+
response: 'Hello World',
|
|
604
|
+
stats: MOCK_SESSION_METRICS,
|
|
605
|
+
}, null, 2));
|
|
606
|
+
});
|
|
607
|
+
it('should write JSON output with stats for tool-only commands (no text response)', async () => {
|
|
608
|
+
// Test the scenario where a command completes successfully with only tool calls
|
|
609
|
+
// but no text response - this would have caught the original bug
|
|
610
|
+
const toolCallEvent = {
|
|
611
|
+
type: GeminiEventType.ToolCallRequest,
|
|
612
|
+
value: {
|
|
613
|
+
callId: 'tool-1',
|
|
614
|
+
name: 'testTool',
|
|
615
|
+
args: { arg1: 'value1' },
|
|
616
|
+
isClientInitiated: false,
|
|
617
|
+
prompt_id: 'prompt-id-tool-only',
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
const toolResponse = [{ text: 'Tool executed successfully' }];
|
|
621
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
622
|
+
{
|
|
623
|
+
status: CoreToolCallStatus.Success,
|
|
624
|
+
request: {
|
|
625
|
+
callId: 'tool-1',
|
|
626
|
+
name: 'testTool',
|
|
627
|
+
args: { arg1: 'value1' },
|
|
628
|
+
isClientInitiated: false,
|
|
629
|
+
prompt_id: 'prompt-id-tool-only',
|
|
630
|
+
},
|
|
631
|
+
tool: {},
|
|
632
|
+
invocation: {},
|
|
633
|
+
response: {
|
|
634
|
+
responseParts: toolResponse,
|
|
635
|
+
callId: 'tool-1',
|
|
636
|
+
error: undefined,
|
|
637
|
+
errorType: undefined,
|
|
638
|
+
contentLength: undefined,
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
]);
|
|
642
|
+
// First call returns only tool call, no content
|
|
643
|
+
const firstCallEvents = [
|
|
644
|
+
toolCallEvent,
|
|
645
|
+
{
|
|
646
|
+
type: GeminiEventType.Finished,
|
|
647
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } },
|
|
648
|
+
},
|
|
649
|
+
];
|
|
650
|
+
// Second call returns no content (tool-only completion)
|
|
651
|
+
const secondCallEvents = [
|
|
652
|
+
{
|
|
653
|
+
type: GeminiEventType.Finished,
|
|
654
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 3 } },
|
|
655
|
+
},
|
|
656
|
+
];
|
|
657
|
+
mockGeminiClient.sendMessageStream
|
|
658
|
+
.mockReturnValueOnce(createStreamFromEvents(firstCallEvents))
|
|
659
|
+
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
|
|
660
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON);
|
|
661
|
+
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(MOCK_SESSION_METRICS);
|
|
662
|
+
await runNonInteractive({
|
|
663
|
+
config: mockConfig,
|
|
664
|
+
settings: mockSettings,
|
|
665
|
+
input: 'Execute tool only',
|
|
666
|
+
prompt_id: 'prompt-id-tool-only',
|
|
667
|
+
});
|
|
668
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
|
|
669
|
+
expect(mockSchedulerSchedule).toHaveBeenCalledWith([expect.objectContaining({ name: 'testTool' })], expect.any(AbortSignal));
|
|
670
|
+
// This should output JSON with empty response but include stats
|
|
671
|
+
expect(processStdoutSpy).toHaveBeenCalledWith(JSON.stringify({
|
|
672
|
+
session_id: 'test-session-id',
|
|
673
|
+
response: '',
|
|
674
|
+
stats: MOCK_SESSION_METRICS,
|
|
675
|
+
}, null, 2));
|
|
676
|
+
});
|
|
677
|
+
it('should keep only the final post-tool assistant text in JSON output', async () => {
|
|
678
|
+
const toolCallEvent = {
|
|
679
|
+
type: GeminiEventType.ToolCallRequest,
|
|
680
|
+
value: {
|
|
681
|
+
callId: 'tool-1',
|
|
682
|
+
name: 'testTool',
|
|
683
|
+
args: { arg1: 'value1' },
|
|
684
|
+
isClientInitiated: false,
|
|
685
|
+
prompt_id: 'prompt-id-json-tool-text',
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
689
|
+
{
|
|
690
|
+
status: CoreToolCallStatus.Success,
|
|
691
|
+
request: toolCallEvent.value,
|
|
692
|
+
tool: {},
|
|
693
|
+
invocation: {},
|
|
694
|
+
response: {
|
|
695
|
+
responseParts: [{ text: 'Tool executed successfully' }],
|
|
696
|
+
callId: 'tool-1',
|
|
697
|
+
error: undefined,
|
|
698
|
+
errorType: undefined,
|
|
699
|
+
contentLength: undefined,
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
]);
|
|
703
|
+
mockGeminiClient.sendMessageStream
|
|
704
|
+
.mockReturnValueOnce(createStreamFromEvents([
|
|
705
|
+
{ type: GeminiEventType.Content, value: 'Let me check that...' },
|
|
706
|
+
toolCallEvent,
|
|
707
|
+
{
|
|
708
|
+
type: GeminiEventType.Finished,
|
|
709
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } },
|
|
710
|
+
},
|
|
711
|
+
]))
|
|
712
|
+
.mockReturnValueOnce(createStreamFromEvents([
|
|
713
|
+
{ type: GeminiEventType.Content, value: 'Final answer' },
|
|
714
|
+
{
|
|
715
|
+
type: GeminiEventType.Finished,
|
|
716
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 3 } },
|
|
717
|
+
},
|
|
718
|
+
]));
|
|
719
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON);
|
|
720
|
+
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(MOCK_SESSION_METRICS);
|
|
721
|
+
await runNonInteractive({
|
|
722
|
+
config: mockConfig,
|
|
723
|
+
settings: mockSettings,
|
|
724
|
+
input: 'Use a tool',
|
|
725
|
+
prompt_id: 'prompt-id-json-tool-text',
|
|
726
|
+
});
|
|
727
|
+
expect(processStdoutSpy).toHaveBeenCalledWith(JSON.stringify({
|
|
728
|
+
session_id: 'test-session-id',
|
|
729
|
+
response: 'Final answer',
|
|
730
|
+
stats: MOCK_SESSION_METRICS,
|
|
731
|
+
}, null, 2));
|
|
732
|
+
});
|
|
733
|
+
it('should write JSON output with stats for empty response commands', async () => {
|
|
734
|
+
// Test the scenario where a command completes but produces no content at all
|
|
735
|
+
const events = [
|
|
736
|
+
{
|
|
737
|
+
type: GeminiEventType.Finished,
|
|
738
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 1 } },
|
|
739
|
+
},
|
|
740
|
+
];
|
|
741
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
742
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON);
|
|
743
|
+
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(MOCK_SESSION_METRICS);
|
|
744
|
+
await runNonInteractive({
|
|
745
|
+
config: mockConfig,
|
|
746
|
+
settings: mockSettings,
|
|
747
|
+
input: 'Empty response test',
|
|
748
|
+
prompt_id: 'prompt-id-empty',
|
|
749
|
+
});
|
|
750
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith([{ text: 'Empty response test' }], expect.any(AbortSignal), 'prompt-id-empty', undefined, false, 'Empty response test');
|
|
751
|
+
// This should output JSON with empty response but include stats
|
|
752
|
+
expect(processStdoutSpy).toHaveBeenCalledWith(JSON.stringify({
|
|
753
|
+
session_id: 'test-session-id',
|
|
754
|
+
response: '',
|
|
755
|
+
stats: MOCK_SESSION_METRICS,
|
|
756
|
+
}, null, 2));
|
|
757
|
+
});
|
|
758
|
+
it('should handle errors in JSON format', async () => {
|
|
759
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON);
|
|
760
|
+
const testError = new Error('Invalid input provided');
|
|
761
|
+
mockGeminiClient.sendMessageStream.mockImplementation(() => {
|
|
762
|
+
throw testError;
|
|
763
|
+
});
|
|
764
|
+
let thrownError = null;
|
|
765
|
+
try {
|
|
766
|
+
await runNonInteractive({
|
|
767
|
+
config: mockConfig,
|
|
768
|
+
settings: mockSettings,
|
|
769
|
+
input: 'Test input',
|
|
770
|
+
prompt_id: 'prompt-id-error',
|
|
771
|
+
});
|
|
772
|
+
// Should not reach here
|
|
773
|
+
expect.fail('Expected process.exit to be called');
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
thrownError = error;
|
|
777
|
+
}
|
|
778
|
+
// Should throw because of mocked process.exit
|
|
779
|
+
expect(thrownError?.message).toBe('process.exit(1) called');
|
|
780
|
+
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith('error', JSON.stringify({
|
|
781
|
+
session_id: 'test-session-id',
|
|
782
|
+
error: {
|
|
783
|
+
type: 'Error',
|
|
784
|
+
message: 'Invalid input provided',
|
|
785
|
+
code: 1,
|
|
786
|
+
},
|
|
787
|
+
}, null, 2));
|
|
788
|
+
});
|
|
789
|
+
it('should handle FatalInputError with custom exit code in JSON format', async () => {
|
|
790
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON);
|
|
791
|
+
const fatalError = new FatalInputError('Invalid command syntax provided');
|
|
792
|
+
mockGeminiClient.sendMessageStream.mockImplementation(() => {
|
|
793
|
+
throw fatalError;
|
|
794
|
+
});
|
|
795
|
+
let thrownError = null;
|
|
796
|
+
try {
|
|
797
|
+
await runNonInteractive({
|
|
798
|
+
config: mockConfig,
|
|
799
|
+
settings: mockSettings,
|
|
800
|
+
input: 'Invalid syntax',
|
|
801
|
+
prompt_id: 'prompt-id-fatal',
|
|
802
|
+
});
|
|
803
|
+
// Should not reach here
|
|
804
|
+
expect.fail('Expected process.exit to be called');
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
thrownError = error;
|
|
808
|
+
}
|
|
809
|
+
// Should throw because of mocked process.exit with custom exit code
|
|
810
|
+
expect(thrownError?.message).toBe('process.exit(42) called');
|
|
811
|
+
expect(mockCoreEvents.emitFeedback).toHaveBeenCalledWith('error', JSON.stringify({
|
|
812
|
+
session_id: 'test-session-id',
|
|
813
|
+
error: {
|
|
814
|
+
type: 'FatalInputError',
|
|
815
|
+
message: 'Invalid command syntax provided',
|
|
816
|
+
code: 42,
|
|
817
|
+
},
|
|
818
|
+
}, null, 2));
|
|
819
|
+
});
|
|
820
|
+
it('should execute a slash command that returns a prompt', async () => {
|
|
821
|
+
const mockCommand = {
|
|
822
|
+
name: 'testcommand',
|
|
823
|
+
description: 'a test command',
|
|
824
|
+
action: vi.fn().mockResolvedValue({
|
|
825
|
+
type: 'submit_prompt',
|
|
826
|
+
content: [{ text: 'Prompt from command' }],
|
|
827
|
+
}),
|
|
828
|
+
};
|
|
829
|
+
mockGetCommands.mockReturnValue([mockCommand]);
|
|
830
|
+
const events = [
|
|
831
|
+
{ type: GeminiEventType.Content, value: 'Response from command' },
|
|
832
|
+
{
|
|
833
|
+
type: GeminiEventType.Finished,
|
|
834
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } },
|
|
835
|
+
},
|
|
836
|
+
];
|
|
837
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
838
|
+
await runNonInteractive({
|
|
839
|
+
config: mockConfig,
|
|
840
|
+
settings: mockSettings,
|
|
841
|
+
input: '/testcommand',
|
|
842
|
+
prompt_id: 'prompt-id-slash',
|
|
843
|
+
});
|
|
844
|
+
// Ensure the prompt sent to the model is from the command, not the raw input
|
|
845
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith([{ text: 'Prompt from command' }], expect.any(AbortSignal), 'prompt-id-slash', undefined, false, '/testcommand');
|
|
846
|
+
expect(getWrittenOutput()).toBe('Response from command\n');
|
|
847
|
+
});
|
|
848
|
+
it('should handle slash commands', async () => {
|
|
849
|
+
const nonInteractiveCliCommands = await import('./nonInteractiveCliCommands.js');
|
|
850
|
+
const handleSlashCommandSpy = vi.spyOn(nonInteractiveCliCommands, 'handleSlashCommand');
|
|
851
|
+
handleSlashCommandSpy.mockResolvedValue([{ text: 'Slash command output' }]);
|
|
852
|
+
const events = [
|
|
853
|
+
{ type: GeminiEventType.Content, value: 'Response to slash command' },
|
|
854
|
+
{
|
|
855
|
+
type: GeminiEventType.Finished,
|
|
856
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
857
|
+
},
|
|
858
|
+
];
|
|
859
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
860
|
+
await runNonInteractive({
|
|
861
|
+
config: mockConfig,
|
|
862
|
+
settings: mockSettings,
|
|
863
|
+
input: '/help',
|
|
864
|
+
prompt_id: 'prompt-id-slash',
|
|
865
|
+
});
|
|
866
|
+
expect(handleSlashCommandSpy).toHaveBeenCalledWith('/help', expect.any(AbortController), mockConfig, mockSettings);
|
|
867
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith([{ text: 'Slash command output' }], expect.any(AbortSignal), 'prompt-id-slash', undefined, false, '/help');
|
|
868
|
+
expect(getWrittenOutput()).toBe('Response to slash command\n');
|
|
869
|
+
handleSlashCommandSpy.mockRestore();
|
|
870
|
+
});
|
|
871
|
+
it('should handle cancellation (Ctrl+C)', async () => {
|
|
872
|
+
// Mock isTTY and setRawMode safely
|
|
873
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
874
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
875
|
+
const originalSetRawMode = process.stdin.setRawMode;
|
|
876
|
+
Object.defineProperty(process.stdin, 'isTTY', {
|
|
877
|
+
value: true,
|
|
878
|
+
configurable: true,
|
|
879
|
+
});
|
|
880
|
+
if (!originalSetRawMode) {
|
|
881
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
882
|
+
process.stdin.setRawMode = vi.fn();
|
|
883
|
+
}
|
|
884
|
+
const stdinOnSpy = vi
|
|
885
|
+
.spyOn(process.stdin, 'on')
|
|
886
|
+
.mockImplementation(() => process.stdin);
|
|
887
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
888
|
+
vi.spyOn(process.stdin, 'setRawMode').mockImplementation(() => true);
|
|
889
|
+
vi.spyOn(process.stdin, 'resume').mockImplementation(() => process.stdin);
|
|
890
|
+
vi.spyOn(process.stdin, 'pause').mockImplementation(() => process.stdin);
|
|
891
|
+
vi.spyOn(process.stdin, 'removeAllListeners').mockImplementation(() => process.stdin);
|
|
892
|
+
// Cancellation will throw FatalCancellationError directly
|
|
893
|
+
const events = [
|
|
894
|
+
{ type: GeminiEventType.Content, value: 'Thinking...' },
|
|
895
|
+
];
|
|
896
|
+
// Create a stream that responds to abortion
|
|
897
|
+
mockGeminiClient.sendMessageStream.mockImplementation((_messages, signal) => (async function* () {
|
|
898
|
+
yield events[0];
|
|
899
|
+
await new Promise((resolve, reject) => {
|
|
900
|
+
const timeout = setTimeout(resolve, 1000);
|
|
901
|
+
signal.addEventListener('abort', () => {
|
|
902
|
+
clearTimeout(timeout);
|
|
903
|
+
setTimeout(() => {
|
|
904
|
+
reject(new Error('Aborted'));
|
|
905
|
+
}, 300);
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
})());
|
|
909
|
+
const runPromise = runNonInteractive({
|
|
910
|
+
config: mockConfig,
|
|
911
|
+
settings: mockSettings,
|
|
912
|
+
input: 'Long running query',
|
|
913
|
+
prompt_id: 'prompt-id-cancel',
|
|
914
|
+
});
|
|
915
|
+
// Wait a bit for setup to complete and listeners to be registered
|
|
916
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
917
|
+
// Find the keypress handler registered by runNonInteractive
|
|
918
|
+
const keypressCall = stdinOnSpy.mock.calls.find(
|
|
919
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
920
|
+
(call) => call[0] === 'keypress');
|
|
921
|
+
expect(keypressCall).toBeDefined();
|
|
922
|
+
const keypressHandler = keypressCall?.[1];
|
|
923
|
+
if (keypressHandler) {
|
|
924
|
+
// Simulate Ctrl+C
|
|
925
|
+
keypressHandler('\u0003', { ctrl: true, name: 'c' });
|
|
926
|
+
}
|
|
927
|
+
await expect(runPromise).rejects.toThrow('Operation cancelled.');
|
|
928
|
+
expect(processStderrSpy.mock.calls.some(
|
|
929
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
930
|
+
(call) => typeof call[0] === 'string' && call[0].includes('Cancelling'))).toBe(true);
|
|
931
|
+
// Restore original values
|
|
932
|
+
Object.defineProperty(process.stdin, 'isTTY', {
|
|
933
|
+
value: originalIsTTY,
|
|
934
|
+
configurable: true,
|
|
935
|
+
});
|
|
936
|
+
if (originalSetRawMode) {
|
|
937
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
938
|
+
process.stdin.setRawMode = originalSetRawMode;
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
942
|
+
delete process.stdin.setRawMode;
|
|
943
|
+
}
|
|
944
|
+
// Spies are automatically restored by vi.restoreAllMocks() in afterEach,
|
|
945
|
+
// but we can also do it manually if needed.
|
|
946
|
+
});
|
|
947
|
+
it('should honor cancellation that happens before session.send()', async () => {
|
|
948
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
949
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
950
|
+
const originalSetRawMode = process.stdin.setRawMode;
|
|
951
|
+
Object.defineProperty(process.stdin, 'isTTY', {
|
|
952
|
+
value: true,
|
|
953
|
+
configurable: true,
|
|
954
|
+
});
|
|
955
|
+
if (!originalSetRawMode) {
|
|
956
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
957
|
+
process.stdin.setRawMode = vi.fn();
|
|
958
|
+
}
|
|
959
|
+
const stdinOnSpy = vi
|
|
960
|
+
.spyOn(process.stdin, 'on')
|
|
961
|
+
.mockImplementation((event, listener) => {
|
|
962
|
+
if (event === 'keypress') {
|
|
963
|
+
listener('\u0003', { ctrl: true, name: 'c' });
|
|
964
|
+
}
|
|
965
|
+
return process.stdin;
|
|
966
|
+
});
|
|
967
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
968
|
+
vi.spyOn(process.stdin, 'setRawMode').mockImplementation(() => true);
|
|
969
|
+
vi.spyOn(process.stdin, 'resume').mockImplementation(() => process.stdin);
|
|
970
|
+
vi.spyOn(process.stdin, 'pause').mockImplementation(() => process.stdin);
|
|
971
|
+
vi.spyOn(process.stdin, 'removeAllListeners').mockImplementation(() => process.stdin);
|
|
972
|
+
// Cancellation will throw FatalCancellationError directly
|
|
973
|
+
const { LegacyAgentSession } = await import('@google/gemini-cli-core');
|
|
974
|
+
const sendSpy = vi.spyOn(LegacyAgentSession.prototype, 'send');
|
|
975
|
+
await expect(runNonInteractive({
|
|
976
|
+
config: mockConfig,
|
|
977
|
+
settings: mockSettings,
|
|
978
|
+
input: 'Cancelled query',
|
|
979
|
+
prompt_id: 'prompt-id-pre-send-cancel',
|
|
980
|
+
})).rejects.toThrow('Operation cancelled.');
|
|
981
|
+
expect(sendSpy).not.toHaveBeenCalled();
|
|
982
|
+
expect(stdinOnSpy).toHaveBeenCalled();
|
|
983
|
+
sendSpy.mockRestore();
|
|
984
|
+
Object.defineProperty(process.stdin, 'isTTY', {
|
|
985
|
+
value: originalIsTTY,
|
|
986
|
+
configurable: true,
|
|
987
|
+
});
|
|
988
|
+
if (originalSetRawMode) {
|
|
989
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
990
|
+
process.stdin.setRawMode = originalSetRawMode;
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
994
|
+
delete process.stdin.setRawMode;
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
it('should throw FatalInputError if a command requires confirmation', async () => {
|
|
998
|
+
const mockCommand = {
|
|
999
|
+
name: 'confirm',
|
|
1000
|
+
description: 'a command that needs confirmation',
|
|
1001
|
+
action: vi.fn().mockResolvedValue({
|
|
1002
|
+
type: 'confirm_shell_commands',
|
|
1003
|
+
commands: ['rm -rf /'],
|
|
1004
|
+
}),
|
|
1005
|
+
};
|
|
1006
|
+
mockGetCommands.mockReturnValue([mockCommand]);
|
|
1007
|
+
await expect(runNonInteractive({
|
|
1008
|
+
config: mockConfig,
|
|
1009
|
+
settings: mockSettings,
|
|
1010
|
+
input: '/confirm',
|
|
1011
|
+
prompt_id: 'prompt-id-confirm',
|
|
1012
|
+
})).rejects.toThrow('Exiting due to a confirmation prompt requested by the command.');
|
|
1013
|
+
});
|
|
1014
|
+
it('should treat an unknown slash command as a regular prompt', async () => {
|
|
1015
|
+
// No commands are mocked, so any slash command is "unknown"
|
|
1016
|
+
mockGetCommands.mockReturnValue([]);
|
|
1017
|
+
const events = [
|
|
1018
|
+
{ type: GeminiEventType.Content, value: 'Response to unknown' },
|
|
1019
|
+
{
|
|
1020
|
+
type: GeminiEventType.Finished,
|
|
1021
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } },
|
|
1022
|
+
},
|
|
1023
|
+
];
|
|
1024
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1025
|
+
await runNonInteractive({
|
|
1026
|
+
config: mockConfig,
|
|
1027
|
+
settings: mockSettings,
|
|
1028
|
+
input: '/unknowncommand',
|
|
1029
|
+
prompt_id: 'prompt-id-unknown',
|
|
1030
|
+
});
|
|
1031
|
+
// Ensure the raw input is sent to the model
|
|
1032
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith([{ text: '/unknowncommand' }], expect.any(AbortSignal), 'prompt-id-unknown', undefined, false, '/unknowncommand');
|
|
1033
|
+
expect(getWrittenOutput()).toBe('Response to unknown\n');
|
|
1034
|
+
});
|
|
1035
|
+
it('should throw for unhandled command result types', async () => {
|
|
1036
|
+
const mockCommand = {
|
|
1037
|
+
name: 'noaction',
|
|
1038
|
+
description: 'unhandled type',
|
|
1039
|
+
action: vi.fn().mockResolvedValue({
|
|
1040
|
+
type: 'unhandled',
|
|
1041
|
+
}),
|
|
1042
|
+
};
|
|
1043
|
+
mockGetCommands.mockReturnValue([mockCommand]);
|
|
1044
|
+
await expect(runNonInteractive({
|
|
1045
|
+
config: mockConfig,
|
|
1046
|
+
settings: mockSettings,
|
|
1047
|
+
input: '/noaction',
|
|
1048
|
+
prompt_id: 'prompt-id-unhandled',
|
|
1049
|
+
})).rejects.toThrow('Exiting due to command result that is not supported in non-interactive mode.');
|
|
1050
|
+
});
|
|
1051
|
+
it('should pass arguments to the slash command action', async () => {
|
|
1052
|
+
const mockAction = vi.fn().mockResolvedValue({
|
|
1053
|
+
type: 'submit_prompt',
|
|
1054
|
+
content: [{ text: 'Prompt from command' }],
|
|
1055
|
+
});
|
|
1056
|
+
const mockCommand = {
|
|
1057
|
+
name: 'testargs',
|
|
1058
|
+
description: 'a test command',
|
|
1059
|
+
action: mockAction,
|
|
1060
|
+
};
|
|
1061
|
+
mockGetCommands.mockReturnValue([mockCommand]);
|
|
1062
|
+
const events = [
|
|
1063
|
+
{ type: GeminiEventType.Content, value: 'Acknowledged' },
|
|
1064
|
+
{
|
|
1065
|
+
type: GeminiEventType.Finished,
|
|
1066
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 1 } },
|
|
1067
|
+
},
|
|
1068
|
+
];
|
|
1069
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1070
|
+
await runNonInteractive({
|
|
1071
|
+
config: mockConfig,
|
|
1072
|
+
settings: mockSettings,
|
|
1073
|
+
input: '/testargs arg1 arg2',
|
|
1074
|
+
prompt_id: 'prompt-id-args',
|
|
1075
|
+
});
|
|
1076
|
+
expect(mockAction).toHaveBeenCalledWith(expect.any(Object), 'arg1 arg2');
|
|
1077
|
+
expect(getWrittenOutput()).toBe('Acknowledged\n');
|
|
1078
|
+
});
|
|
1079
|
+
it('should instantiate CommandService with correct loaders for slash commands', async () => {
|
|
1080
|
+
// This test indirectly checks that handleSlashCommand is using the right loaders.
|
|
1081
|
+
const { FileCommandLoader } = await import('./services/FileCommandLoader.js');
|
|
1082
|
+
const { McpPromptLoader } = await import('./services/McpPromptLoader.js');
|
|
1083
|
+
const { BuiltinCommandLoader } = await import('./services/BuiltinCommandLoader.js');
|
|
1084
|
+
mockGetCommands.mockReturnValue([]); // No commands found, so it will fall through
|
|
1085
|
+
const events = [
|
|
1086
|
+
{ type: GeminiEventType.Content, value: 'Acknowledged' },
|
|
1087
|
+
{
|
|
1088
|
+
type: GeminiEventType.Finished,
|
|
1089
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 1 } },
|
|
1090
|
+
},
|
|
1091
|
+
];
|
|
1092
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1093
|
+
await runNonInteractive({
|
|
1094
|
+
config: mockConfig,
|
|
1095
|
+
settings: mockSettings,
|
|
1096
|
+
input: '/mycommand',
|
|
1097
|
+
prompt_id: 'prompt-id-loaders',
|
|
1098
|
+
});
|
|
1099
|
+
// Check that loaders were instantiated with the config
|
|
1100
|
+
expect(FileCommandLoader).toHaveBeenCalledTimes(1);
|
|
1101
|
+
expect(FileCommandLoader).toHaveBeenCalledWith(mockConfig);
|
|
1102
|
+
expect(McpPromptLoader).toHaveBeenCalledTimes(1);
|
|
1103
|
+
expect(McpPromptLoader).toHaveBeenCalledWith(mockConfig);
|
|
1104
|
+
expect(BuiltinCommandLoader).toHaveBeenCalledWith(mockConfig);
|
|
1105
|
+
// Check that instances were passed to CommandService.create
|
|
1106
|
+
expect(mockCommandServiceCreate).toHaveBeenCalledTimes(1);
|
|
1107
|
+
const loadersArg = mockCommandServiceCreate.mock.calls[0][0];
|
|
1108
|
+
expect(loadersArg).toHaveLength(3);
|
|
1109
|
+
expect(loadersArg[0]).toBe(vi.mocked(BuiltinCommandLoader).mock.instances[0]);
|
|
1110
|
+
expect(loadersArg[1]).toBe(vi.mocked(McpPromptLoader).mock.instances[0]);
|
|
1111
|
+
expect(loadersArg[2]).toBe(vi.mocked(FileCommandLoader).mock.instances[0]);
|
|
1112
|
+
});
|
|
1113
|
+
it('should allow a normally-excluded tool when --allowed-tools is set', async () => {
|
|
1114
|
+
// By default, ShellTool is excluded in non-interactive mode.
|
|
1115
|
+
// This test ensures that --allowed-tools overrides this exclusion.
|
|
1116
|
+
vi.mocked(mockConfig.getToolRegistry).mockReturnValue({
|
|
1117
|
+
getTool: vi.fn().mockReturnValue({
|
|
1118
|
+
name: 'ShellTool',
|
|
1119
|
+
description: 'A shell tool',
|
|
1120
|
+
run: vi.fn(),
|
|
1121
|
+
}),
|
|
1122
|
+
getFunctionDeclarations: vi.fn().mockReturnValue([{ name: 'ShellTool' }]),
|
|
1123
|
+
});
|
|
1124
|
+
const toolCallEvent = {
|
|
1125
|
+
type: GeminiEventType.ToolCallRequest,
|
|
1126
|
+
value: {
|
|
1127
|
+
callId: 'tool-shell-1',
|
|
1128
|
+
name: 'ShellTool',
|
|
1129
|
+
args: { command: 'ls' },
|
|
1130
|
+
isClientInitiated: false,
|
|
1131
|
+
prompt_id: 'prompt-id-allowed',
|
|
1132
|
+
},
|
|
1133
|
+
};
|
|
1134
|
+
const toolResponse = [{ text: 'file.txt' }];
|
|
1135
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
1136
|
+
{
|
|
1137
|
+
status: CoreToolCallStatus.Success,
|
|
1138
|
+
request: {
|
|
1139
|
+
callId: 'tool-shell-1',
|
|
1140
|
+
name: 'ShellTool',
|
|
1141
|
+
args: { command: 'ls' },
|
|
1142
|
+
isClientInitiated: false,
|
|
1143
|
+
prompt_id: 'prompt-id-allowed',
|
|
1144
|
+
},
|
|
1145
|
+
tool: {},
|
|
1146
|
+
invocation: {},
|
|
1147
|
+
response: {
|
|
1148
|
+
responseParts: toolResponse,
|
|
1149
|
+
callId: 'tool-shell-1',
|
|
1150
|
+
error: undefined,
|
|
1151
|
+
errorType: undefined,
|
|
1152
|
+
contentLength: undefined,
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
]);
|
|
1156
|
+
const firstCallEvents = [toolCallEvent];
|
|
1157
|
+
const secondCallEvents = [
|
|
1158
|
+
{ type: GeminiEventType.Content, value: 'file.txt' },
|
|
1159
|
+
{
|
|
1160
|
+
type: GeminiEventType.Finished,
|
|
1161
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
1162
|
+
},
|
|
1163
|
+
];
|
|
1164
|
+
mockGeminiClient.sendMessageStream
|
|
1165
|
+
.mockReturnValueOnce(createStreamFromEvents(firstCallEvents))
|
|
1166
|
+
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
|
|
1167
|
+
await runNonInteractive({
|
|
1168
|
+
config: mockConfig,
|
|
1169
|
+
settings: mockSettings,
|
|
1170
|
+
input: 'List the files',
|
|
1171
|
+
prompt_id: 'prompt-id-allowed',
|
|
1172
|
+
});
|
|
1173
|
+
expect(mockSchedulerSchedule).toHaveBeenCalledWith([expect.objectContaining({ name: 'ShellTool' })], expect.any(AbortSignal));
|
|
1174
|
+
expect(getWrittenOutput()).toBe('file.txt\n');
|
|
1175
|
+
});
|
|
1176
|
+
describe('CoreEvents Integration', () => {
|
|
1177
|
+
it('subscribes to UserFeedback and drains backlog on start', async () => {
|
|
1178
|
+
const events = [
|
|
1179
|
+
{
|
|
1180
|
+
type: GeminiEventType.Finished,
|
|
1181
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
|
1182
|
+
},
|
|
1183
|
+
];
|
|
1184
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1185
|
+
await runNonInteractive({
|
|
1186
|
+
config: mockConfig,
|
|
1187
|
+
settings: mockSettings,
|
|
1188
|
+
input: 'test',
|
|
1189
|
+
prompt_id: 'prompt-id-events',
|
|
1190
|
+
});
|
|
1191
|
+
expect(mockCoreEvents.on).toHaveBeenCalledWith(CoreEvent.UserFeedback, expect.any(Function));
|
|
1192
|
+
expect(mockCoreEvents.drainBacklogs).toHaveBeenCalledTimes(1);
|
|
1193
|
+
});
|
|
1194
|
+
it('unsubscribes from UserFeedback on finish', async () => {
|
|
1195
|
+
const events = [
|
|
1196
|
+
{
|
|
1197
|
+
type: GeminiEventType.Finished,
|
|
1198
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
|
1199
|
+
},
|
|
1200
|
+
];
|
|
1201
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1202
|
+
await runNonInteractive({
|
|
1203
|
+
config: mockConfig,
|
|
1204
|
+
settings: mockSettings,
|
|
1205
|
+
input: 'test',
|
|
1206
|
+
prompt_id: 'prompt-id-events',
|
|
1207
|
+
});
|
|
1208
|
+
expect(mockCoreEvents.off).toHaveBeenCalledWith(CoreEvent.UserFeedback, expect.any(Function));
|
|
1209
|
+
});
|
|
1210
|
+
it('logs to process.stderr when UserFeedback event is received', async () => {
|
|
1211
|
+
const events = [
|
|
1212
|
+
{
|
|
1213
|
+
type: GeminiEventType.Finished,
|
|
1214
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
|
1215
|
+
},
|
|
1216
|
+
];
|
|
1217
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1218
|
+
await runNonInteractive({
|
|
1219
|
+
config: mockConfig,
|
|
1220
|
+
settings: mockSettings,
|
|
1221
|
+
input: 'test',
|
|
1222
|
+
prompt_id: 'prompt-id-events',
|
|
1223
|
+
});
|
|
1224
|
+
// Get the registered handler
|
|
1225
|
+
const handler = mockCoreEvents.on.mock.calls.find((call) => call[0] === CoreEvent.UserFeedback)?.[1];
|
|
1226
|
+
expect(handler).toBeDefined();
|
|
1227
|
+
// Simulate an event
|
|
1228
|
+
const payload = {
|
|
1229
|
+
severity: 'error',
|
|
1230
|
+
message: 'Test error message',
|
|
1231
|
+
};
|
|
1232
|
+
handler(payload);
|
|
1233
|
+
expect(processStderrSpy).toHaveBeenCalledWith('[ERROR] Test error message\n');
|
|
1234
|
+
});
|
|
1235
|
+
it('logs optional error object to process.stderr in debug mode', async () => {
|
|
1236
|
+
vi.mocked(mockConfig.getDebugMode).mockReturnValue(true);
|
|
1237
|
+
const events = [
|
|
1238
|
+
{
|
|
1239
|
+
type: GeminiEventType.Finished,
|
|
1240
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
|
1241
|
+
},
|
|
1242
|
+
];
|
|
1243
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1244
|
+
await runNonInteractive({
|
|
1245
|
+
config: mockConfig,
|
|
1246
|
+
settings: mockSettings,
|
|
1247
|
+
input: 'test',
|
|
1248
|
+
prompt_id: 'prompt-id-events',
|
|
1249
|
+
});
|
|
1250
|
+
// Get the registered handler
|
|
1251
|
+
const handler = mockCoreEvents.on.mock.calls.find((call) => call[0] === CoreEvent.UserFeedback)?.[1];
|
|
1252
|
+
expect(handler).toBeDefined();
|
|
1253
|
+
// Simulate an event with error object
|
|
1254
|
+
const errorObj = new Error('Original error');
|
|
1255
|
+
// Mock stack for deterministic testing
|
|
1256
|
+
errorObj.stack = 'Error: Original error\n at test';
|
|
1257
|
+
const payload = {
|
|
1258
|
+
severity: 'warning',
|
|
1259
|
+
message: 'Test warning message',
|
|
1260
|
+
error: errorObj,
|
|
1261
|
+
};
|
|
1262
|
+
handler(payload);
|
|
1263
|
+
expect(processStderrSpy).toHaveBeenCalledWith('[WARNING] Test warning message\n');
|
|
1264
|
+
expect(processStderrSpy).toHaveBeenCalledWith('Error: Original error\n at test\n');
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
it('should emit appropriate events for streaming JSON output', async () => {
|
|
1268
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.STREAM_JSON);
|
|
1269
|
+
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(MOCK_SESSION_METRICS);
|
|
1270
|
+
const toolCallEvent = {
|
|
1271
|
+
type: GeminiEventType.ToolCallRequest,
|
|
1272
|
+
value: {
|
|
1273
|
+
callId: 'tool-1',
|
|
1274
|
+
name: 'testTool',
|
|
1275
|
+
args: { arg1: 'value1' },
|
|
1276
|
+
isClientInitiated: false,
|
|
1277
|
+
prompt_id: 'prompt-id-stream',
|
|
1278
|
+
},
|
|
1279
|
+
};
|
|
1280
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
1281
|
+
{
|
|
1282
|
+
status: CoreToolCallStatus.Success,
|
|
1283
|
+
request: toolCallEvent.value,
|
|
1284
|
+
tool: {},
|
|
1285
|
+
invocation: {},
|
|
1286
|
+
response: {
|
|
1287
|
+
responseParts: [{ text: 'Tool response' }],
|
|
1288
|
+
callId: 'tool-1',
|
|
1289
|
+
error: undefined,
|
|
1290
|
+
errorType: undefined,
|
|
1291
|
+
contentLength: undefined,
|
|
1292
|
+
resultDisplay: 'Tool executed successfully',
|
|
1293
|
+
},
|
|
1294
|
+
},
|
|
1295
|
+
]);
|
|
1296
|
+
const firstCallEvents = [
|
|
1297
|
+
{ type: GeminiEventType.Content, value: 'Thinking...' },
|
|
1298
|
+
toolCallEvent,
|
|
1299
|
+
];
|
|
1300
|
+
const secondCallEvents = [
|
|
1301
|
+
{ type: GeminiEventType.Content, value: 'Final answer' },
|
|
1302
|
+
{
|
|
1303
|
+
type: GeminiEventType.Finished,
|
|
1304
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
1305
|
+
},
|
|
1306
|
+
];
|
|
1307
|
+
mockGeminiClient.sendMessageStream
|
|
1308
|
+
.mockReturnValueOnce(createStreamFromEvents(firstCallEvents))
|
|
1309
|
+
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
|
|
1310
|
+
await runNonInteractive({
|
|
1311
|
+
config: mockConfig,
|
|
1312
|
+
settings: mockSettings,
|
|
1313
|
+
input: 'Stream test',
|
|
1314
|
+
prompt_id: 'prompt-id-stream',
|
|
1315
|
+
});
|
|
1316
|
+
const output = getWrittenOutput();
|
|
1317
|
+
const sanitizedOutput = output
|
|
1318
|
+
.replace(/"timestamp":"[^"]+"/g, '"timestamp":"<TIMESTAMP>"')
|
|
1319
|
+
.replace(/"duration_ms":\d+/g, '"duration_ms":<DURATION>');
|
|
1320
|
+
expect(sanitizedOutput).toMatchSnapshot();
|
|
1321
|
+
});
|
|
1322
|
+
it('should handle EPIPE error gracefully', async () => {
|
|
1323
|
+
const events = [
|
|
1324
|
+
{ type: GeminiEventType.Content, value: 'Hello' },
|
|
1325
|
+
{ type: GeminiEventType.Content, value: ' World' },
|
|
1326
|
+
];
|
|
1327
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1328
|
+
// Mock process.exit to track calls without throwing
|
|
1329
|
+
vi.spyOn(process, 'exit').mockImplementation((_code) => undefined);
|
|
1330
|
+
// Simulate EPIPE error on stdout
|
|
1331
|
+
const stdoutErrorCallback = process.stdout.on.mock.calls.find((call) => call[0] === 'error')?.[1];
|
|
1332
|
+
if (stdoutErrorCallback) {
|
|
1333
|
+
stdoutErrorCallback({ code: 'EPIPE' });
|
|
1334
|
+
}
|
|
1335
|
+
await runNonInteractive({
|
|
1336
|
+
config: mockConfig,
|
|
1337
|
+
settings: mockSettings,
|
|
1338
|
+
input: 'EPIPE test',
|
|
1339
|
+
prompt_id: 'prompt-id-epipe',
|
|
1340
|
+
});
|
|
1341
|
+
// Since EPIPE is simulated, it might exit early or continue depending on timing,
|
|
1342
|
+
// but our main goal is to verify the handler is registered and handles EPIPE.
|
|
1343
|
+
expect(process.stdout.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
1344
|
+
});
|
|
1345
|
+
it('should resume chat when resumedSessionData is provided', async () => {
|
|
1346
|
+
const events = [
|
|
1347
|
+
{ type: GeminiEventType.Content, value: 'Resumed' },
|
|
1348
|
+
{
|
|
1349
|
+
type: GeminiEventType.Finished,
|
|
1350
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } },
|
|
1351
|
+
},
|
|
1352
|
+
];
|
|
1353
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1354
|
+
const resumedSessionData = {
|
|
1355
|
+
conversation: {
|
|
1356
|
+
sessionId: 'resumed-session-id',
|
|
1357
|
+
messages: [
|
|
1358
|
+
{ role: 'user', parts: [{ text: 'Previous message' }] },
|
|
1359
|
+
], // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
1360
|
+
startTime: new Date().toISOString(),
|
|
1361
|
+
lastUpdated: new Date().toISOString(),
|
|
1362
|
+
firstUserMessage: 'Previous message',
|
|
1363
|
+
projectHash: 'test-hash',
|
|
1364
|
+
},
|
|
1365
|
+
filePath: '/path/to/session.json',
|
|
1366
|
+
};
|
|
1367
|
+
await runNonInteractive({
|
|
1368
|
+
config: mockConfig,
|
|
1369
|
+
settings: mockSettings,
|
|
1370
|
+
input: 'Continue',
|
|
1371
|
+
prompt_id: 'prompt-id-resume',
|
|
1372
|
+
resumedSessionData,
|
|
1373
|
+
});
|
|
1374
|
+
expect(mockGeminiClient.resumeChat).toHaveBeenCalledWith(expect.any(Array), resumedSessionData);
|
|
1375
|
+
expect(getWrittenOutput()).toBe('Resumed\n');
|
|
1376
|
+
});
|
|
1377
|
+
it.each([
|
|
1378
|
+
{
|
|
1379
|
+
name: 'loop detected',
|
|
1380
|
+
events: [
|
|
1381
|
+
{ type: GeminiEventType.LoopDetected },
|
|
1382
|
+
],
|
|
1383
|
+
input: 'Loop test',
|
|
1384
|
+
promptId: 'prompt-id-loop',
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
name: 'max session turns',
|
|
1388
|
+
events: [
|
|
1389
|
+
{ type: GeminiEventType.MaxSessionTurns },
|
|
1390
|
+
],
|
|
1391
|
+
input: 'Max turns test',
|
|
1392
|
+
promptId: 'prompt-id-max-turns',
|
|
1393
|
+
},
|
|
1394
|
+
])('should emit appropriate error event in streaming JSON mode: $name', async ({ events, input, promptId }) => {
|
|
1395
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.STREAM_JSON);
|
|
1396
|
+
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(MOCK_SESSION_METRICS);
|
|
1397
|
+
const streamEvents = [
|
|
1398
|
+
...events,
|
|
1399
|
+
{
|
|
1400
|
+
type: GeminiEventType.Finished,
|
|
1401
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
|
1402
|
+
},
|
|
1403
|
+
];
|
|
1404
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(streamEvents));
|
|
1405
|
+
try {
|
|
1406
|
+
await runNonInteractive({
|
|
1407
|
+
config: mockConfig,
|
|
1408
|
+
settings: mockSettings,
|
|
1409
|
+
input,
|
|
1410
|
+
prompt_id: promptId,
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
catch {
|
|
1414
|
+
// Expected exit
|
|
1415
|
+
}
|
|
1416
|
+
const output = getWrittenOutput();
|
|
1417
|
+
const sanitizedOutput = output
|
|
1418
|
+
.replace(/"timestamp":"[^"]+"/g, '"timestamp":"<TIMESTAMP>"')
|
|
1419
|
+
.replace(/"duration_ms":\d+/g, '"duration_ms":<DURATION>');
|
|
1420
|
+
expect(sanitizedOutput).toMatchSnapshot();
|
|
1421
|
+
});
|
|
1422
|
+
it('should log error when tool recording fails', async () => {
|
|
1423
|
+
const toolCallEvent = {
|
|
1424
|
+
type: GeminiEventType.ToolCallRequest,
|
|
1425
|
+
value: {
|
|
1426
|
+
callId: 'tool-1',
|
|
1427
|
+
name: 'testTool',
|
|
1428
|
+
args: {},
|
|
1429
|
+
isClientInitiated: false,
|
|
1430
|
+
prompt_id: 'prompt-id-tool-error',
|
|
1431
|
+
},
|
|
1432
|
+
};
|
|
1433
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
1434
|
+
{
|
|
1435
|
+
status: CoreToolCallStatus.Success,
|
|
1436
|
+
request: toolCallEvent.value,
|
|
1437
|
+
tool: {},
|
|
1438
|
+
invocation: {},
|
|
1439
|
+
response: {
|
|
1440
|
+
responseParts: [],
|
|
1441
|
+
callId: 'tool-1',
|
|
1442
|
+
error: undefined,
|
|
1443
|
+
errorType: undefined,
|
|
1444
|
+
contentLength: undefined,
|
|
1445
|
+
},
|
|
1446
|
+
},
|
|
1447
|
+
]);
|
|
1448
|
+
const events = [
|
|
1449
|
+
toolCallEvent,
|
|
1450
|
+
{ type: GeminiEventType.Content, value: 'Done' },
|
|
1451
|
+
{
|
|
1452
|
+
type: GeminiEventType.Finished,
|
|
1453
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } },
|
|
1454
|
+
},
|
|
1455
|
+
];
|
|
1456
|
+
mockGeminiClient.sendMessageStream
|
|
1457
|
+
.mockReturnValueOnce(createStreamFromEvents(events))
|
|
1458
|
+
.mockReturnValueOnce(createStreamFromEvents([
|
|
1459
|
+
{ type: GeminiEventType.Content, value: 'Done' },
|
|
1460
|
+
{
|
|
1461
|
+
type: GeminiEventType.Finished,
|
|
1462
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } },
|
|
1463
|
+
},
|
|
1464
|
+
]));
|
|
1465
|
+
// Mock getChat to throw when recording tool calls
|
|
1466
|
+
const mockChat = {
|
|
1467
|
+
recordCompletedToolCalls: vi.fn().mockImplementation(() => {
|
|
1468
|
+
throw new Error('Recording failed');
|
|
1469
|
+
}),
|
|
1470
|
+
};
|
|
1471
|
+
mockGeminiClient.getChat = vi.fn().mockReturnValue(mockChat);
|
|
1472
|
+
mockGeminiClient.getCurrentSequenceModel = vi
|
|
1473
|
+
.fn()
|
|
1474
|
+
.mockReturnValue('model-1');
|
|
1475
|
+
// Mock debugLogger.error
|
|
1476
|
+
const { debugLogger } = await import('@google/gemini-cli-core');
|
|
1477
|
+
const debugLoggerErrorSpy = vi
|
|
1478
|
+
.spyOn(debugLogger, 'error')
|
|
1479
|
+
.mockImplementation(() => { });
|
|
1480
|
+
await runNonInteractive({
|
|
1481
|
+
config: mockConfig,
|
|
1482
|
+
settings: mockSettings,
|
|
1483
|
+
input: 'Tool recording error test',
|
|
1484
|
+
prompt_id: 'prompt-id-tool-error',
|
|
1485
|
+
});
|
|
1486
|
+
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Error recording completed tool call information: Error: Recording failed'));
|
|
1487
|
+
expect(getWrittenOutput()).toContain('Done');
|
|
1488
|
+
});
|
|
1489
|
+
it('should stop agent execution immediately when a tool call returns STOP_EXECUTION error', async () => {
|
|
1490
|
+
const toolCallEvent = {
|
|
1491
|
+
type: GeminiEventType.ToolCallRequest,
|
|
1492
|
+
value: {
|
|
1493
|
+
callId: 'stop-call',
|
|
1494
|
+
name: 'stopTool',
|
|
1495
|
+
args: {},
|
|
1496
|
+
isClientInitiated: false,
|
|
1497
|
+
prompt_id: 'prompt-id-stop',
|
|
1498
|
+
},
|
|
1499
|
+
};
|
|
1500
|
+
// Mock tool execution returning STOP_EXECUTION
|
|
1501
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
1502
|
+
{
|
|
1503
|
+
status: CoreToolCallStatus.Error,
|
|
1504
|
+
request: toolCallEvent.value,
|
|
1505
|
+
tool: {},
|
|
1506
|
+
invocation: {},
|
|
1507
|
+
response: {
|
|
1508
|
+
callId: 'stop-call',
|
|
1509
|
+
responseParts: [{ text: 'error occurred' }],
|
|
1510
|
+
errorType: ToolErrorType.STOP_EXECUTION,
|
|
1511
|
+
error: new Error('Stop reason from hook'),
|
|
1512
|
+
resultDisplay: undefined,
|
|
1513
|
+
},
|
|
1514
|
+
},
|
|
1515
|
+
]);
|
|
1516
|
+
const firstCallEvents = [
|
|
1517
|
+
{ type: GeminiEventType.Content, value: 'Executing tool...' },
|
|
1518
|
+
toolCallEvent,
|
|
1519
|
+
];
|
|
1520
|
+
// Setup the mock to return events for the first call.
|
|
1521
|
+
// We expect the loop to terminate after the tool execution.
|
|
1522
|
+
// If it doesn't, it might call sendMessageStream again, which we'll assert against.
|
|
1523
|
+
mockGeminiClient.sendMessageStream
|
|
1524
|
+
.mockReturnValueOnce(createStreamFromEvents(firstCallEvents))
|
|
1525
|
+
.mockReturnValueOnce(createStreamFromEvents([]));
|
|
1526
|
+
await runNonInteractive({
|
|
1527
|
+
config: mockConfig,
|
|
1528
|
+
settings: mockSettings,
|
|
1529
|
+
input: 'Run stop tool',
|
|
1530
|
+
prompt_id: 'prompt-id-stop',
|
|
1531
|
+
});
|
|
1532
|
+
expect(mockSchedulerSchedule).toHaveBeenCalled();
|
|
1533
|
+
// The key assertion: sendMessageStream should have been called ONLY ONCE (initial user input).
|
|
1534
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(1);
|
|
1535
|
+
expect(processStderrSpy).toHaveBeenCalledWith('Agent execution stopped: Stop reason from hook\n');
|
|
1536
|
+
});
|
|
1537
|
+
it('should write JSON output when a tool call returns STOP_EXECUTION error', async () => {
|
|
1538
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON);
|
|
1539
|
+
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(MOCK_SESSION_METRICS);
|
|
1540
|
+
const toolCallEvent = {
|
|
1541
|
+
type: GeminiEventType.ToolCallRequest,
|
|
1542
|
+
value: {
|
|
1543
|
+
callId: 'stop-call',
|
|
1544
|
+
name: 'stopTool',
|
|
1545
|
+
args: {},
|
|
1546
|
+
isClientInitiated: false,
|
|
1547
|
+
prompt_id: 'prompt-id-stop-json',
|
|
1548
|
+
},
|
|
1549
|
+
};
|
|
1550
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
1551
|
+
{
|
|
1552
|
+
status: CoreToolCallStatus.Error,
|
|
1553
|
+
request: toolCallEvent.value,
|
|
1554
|
+
tool: {},
|
|
1555
|
+
invocation: {},
|
|
1556
|
+
response: {
|
|
1557
|
+
callId: 'stop-call',
|
|
1558
|
+
responseParts: [{ text: 'error occurred' }],
|
|
1559
|
+
errorType: ToolErrorType.STOP_EXECUTION,
|
|
1560
|
+
error: new Error('Stop reason'),
|
|
1561
|
+
resultDisplay: undefined,
|
|
1562
|
+
},
|
|
1563
|
+
},
|
|
1564
|
+
]);
|
|
1565
|
+
const firstCallEvents = [
|
|
1566
|
+
{ type: GeminiEventType.Content, value: 'Partial content' },
|
|
1567
|
+
toolCallEvent,
|
|
1568
|
+
];
|
|
1569
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(firstCallEvents));
|
|
1570
|
+
await runNonInteractive({
|
|
1571
|
+
config: mockConfig,
|
|
1572
|
+
settings: mockSettings,
|
|
1573
|
+
input: 'Run stop tool',
|
|
1574
|
+
prompt_id: 'prompt-id-stop-json',
|
|
1575
|
+
});
|
|
1576
|
+
expect(processStdoutSpy).toHaveBeenCalledWith(JSON.stringify({
|
|
1577
|
+
session_id: 'test-session-id',
|
|
1578
|
+
response: 'Partial content',
|
|
1579
|
+
stats: MOCK_SESSION_METRICS,
|
|
1580
|
+
}, null, 2));
|
|
1581
|
+
});
|
|
1582
|
+
it('should emit result event when a tool call returns STOP_EXECUTION error in streaming JSON mode', async () => {
|
|
1583
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.STREAM_JSON);
|
|
1584
|
+
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(MOCK_SESSION_METRICS);
|
|
1585
|
+
const toolCallEvent = {
|
|
1586
|
+
type: GeminiEventType.ToolCallRequest,
|
|
1587
|
+
value: {
|
|
1588
|
+
callId: 'stop-call',
|
|
1589
|
+
name: 'stopTool',
|
|
1590
|
+
args: {},
|
|
1591
|
+
isClientInitiated: false,
|
|
1592
|
+
prompt_id: 'prompt-id-stop-stream',
|
|
1593
|
+
},
|
|
1594
|
+
};
|
|
1595
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
1596
|
+
{
|
|
1597
|
+
status: CoreToolCallStatus.Error,
|
|
1598
|
+
request: toolCallEvent.value,
|
|
1599
|
+
tool: {},
|
|
1600
|
+
invocation: {},
|
|
1601
|
+
response: {
|
|
1602
|
+
callId: 'stop-call',
|
|
1603
|
+
responseParts: [{ text: 'error occurred' }],
|
|
1604
|
+
errorType: ToolErrorType.STOP_EXECUTION,
|
|
1605
|
+
error: new Error('Stop reason'),
|
|
1606
|
+
resultDisplay: undefined,
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
]);
|
|
1610
|
+
const firstCallEvents = [toolCallEvent];
|
|
1611
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(firstCallEvents));
|
|
1612
|
+
await runNonInteractive({
|
|
1613
|
+
config: mockConfig,
|
|
1614
|
+
settings: mockSettings,
|
|
1615
|
+
input: 'Run stop tool',
|
|
1616
|
+
prompt_id: 'prompt-id-stop-stream',
|
|
1617
|
+
});
|
|
1618
|
+
const output = getWrittenOutput();
|
|
1619
|
+
expect(output).toContain('"type":"result"');
|
|
1620
|
+
expect(output).toContain('"status":"success"');
|
|
1621
|
+
});
|
|
1622
|
+
describe('Agent Execution Events', () => {
|
|
1623
|
+
it('should handle AgentExecutionStopped event', async () => {
|
|
1624
|
+
const events = [
|
|
1625
|
+
{
|
|
1626
|
+
type: GeminiEventType.AgentExecutionStopped,
|
|
1627
|
+
value: { reason: 'Stopped by hook' },
|
|
1628
|
+
},
|
|
1629
|
+
];
|
|
1630
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1631
|
+
await runNonInteractive({
|
|
1632
|
+
config: mockConfig,
|
|
1633
|
+
settings: mockSettings,
|
|
1634
|
+
input: 'test stop',
|
|
1635
|
+
prompt_id: 'prompt-id-stop',
|
|
1636
|
+
});
|
|
1637
|
+
expect(processStderrSpy).toHaveBeenCalledWith('Agent execution stopped: Stopped by hook\n');
|
|
1638
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(1);
|
|
1639
|
+
});
|
|
1640
|
+
it('should handle AgentExecutionBlocked event', async () => {
|
|
1641
|
+
const allEvents = [
|
|
1642
|
+
{
|
|
1643
|
+
type: GeminiEventType.AgentExecutionBlocked,
|
|
1644
|
+
value: { reason: 'Blocked by hook' },
|
|
1645
|
+
},
|
|
1646
|
+
{ type: GeminiEventType.Content, value: 'Final answer' },
|
|
1647
|
+
{
|
|
1648
|
+
type: GeminiEventType.Finished,
|
|
1649
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
1650
|
+
},
|
|
1651
|
+
];
|
|
1652
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(allEvents));
|
|
1653
|
+
await runNonInteractive({
|
|
1654
|
+
config: mockConfig,
|
|
1655
|
+
settings: mockSettings,
|
|
1656
|
+
input: 'test block',
|
|
1657
|
+
prompt_id: 'prompt-id-block',
|
|
1658
|
+
});
|
|
1659
|
+
expect(processStderrSpy).toHaveBeenCalledWith('[WARNING] Agent execution blocked: Blocked by hook\n');
|
|
1660
|
+
// Stream continues after blocked event — content should be output
|
|
1661
|
+
expect(getWrittenOutput()).toBe('Final answer\n');
|
|
1662
|
+
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(1);
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
describe('Output Sanitization', () => {
|
|
1666
|
+
const ANSI_SEQUENCE = '\u001B[31mRed Text\u001B[0m';
|
|
1667
|
+
const OSC_HYPERLINK = '\u001B]8;;http://example.com\u001B\\Link\u001B]8;;\u001B\\';
|
|
1668
|
+
const PLAIN_TEXT_RED = 'Red Text';
|
|
1669
|
+
const PLAIN_TEXT_LINK = 'Link';
|
|
1670
|
+
it('should sanitize ANSI output by default', async () => {
|
|
1671
|
+
const events = [
|
|
1672
|
+
{ type: GeminiEventType.Content, value: ANSI_SEQUENCE },
|
|
1673
|
+
{ type: GeminiEventType.Content, value: ' ' },
|
|
1674
|
+
{ type: GeminiEventType.Content, value: OSC_HYPERLINK },
|
|
1675
|
+
{
|
|
1676
|
+
type: GeminiEventType.Finished,
|
|
1677
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
1678
|
+
},
|
|
1679
|
+
];
|
|
1680
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1681
|
+
vi.mocked(mockConfig.getRawOutput).mockReturnValue(false);
|
|
1682
|
+
await runNonInteractive({
|
|
1683
|
+
config: mockConfig,
|
|
1684
|
+
settings: mockSettings,
|
|
1685
|
+
input: 'Test input',
|
|
1686
|
+
prompt_id: 'prompt-id-sanitization',
|
|
1687
|
+
});
|
|
1688
|
+
expect(getWrittenOutput()).toBe(`${PLAIN_TEXT_RED} ${PLAIN_TEXT_LINK}\n`);
|
|
1689
|
+
});
|
|
1690
|
+
it('should allow ANSI output when rawOutput is true', async () => {
|
|
1691
|
+
const events = [
|
|
1692
|
+
{ type: GeminiEventType.Content, value: ANSI_SEQUENCE },
|
|
1693
|
+
{ type: GeminiEventType.Content, value: ' ' },
|
|
1694
|
+
{ type: GeminiEventType.Content, value: OSC_HYPERLINK },
|
|
1695
|
+
{
|
|
1696
|
+
type: GeminiEventType.Finished,
|
|
1697
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
|
|
1698
|
+
},
|
|
1699
|
+
];
|
|
1700
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1701
|
+
vi.mocked(mockConfig.getRawOutput).mockReturnValue(true);
|
|
1702
|
+
vi.mocked(mockConfig.getAcceptRawOutputRisk).mockReturnValue(true);
|
|
1703
|
+
await runNonInteractive({
|
|
1704
|
+
config: mockConfig,
|
|
1705
|
+
settings: mockSettings,
|
|
1706
|
+
input: 'Test input',
|
|
1707
|
+
prompt_id: 'prompt-id-raw',
|
|
1708
|
+
});
|
|
1709
|
+
expect(getWrittenOutput()).toBe(`${ANSI_SEQUENCE} ${OSC_HYPERLINK}\n`);
|
|
1710
|
+
});
|
|
1711
|
+
it('should allow ANSI output when only acceptRawOutputRisk is true', async () => {
|
|
1712
|
+
const events = [
|
|
1713
|
+
{ type: GeminiEventType.Content, value: ANSI_SEQUENCE },
|
|
1714
|
+
{
|
|
1715
|
+
type: GeminiEventType.Finished,
|
|
1716
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } },
|
|
1717
|
+
},
|
|
1718
|
+
];
|
|
1719
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1720
|
+
vi.mocked(mockConfig.getRawOutput).mockReturnValue(false);
|
|
1721
|
+
vi.mocked(mockConfig.getAcceptRawOutputRisk).mockReturnValue(true);
|
|
1722
|
+
await runNonInteractive({
|
|
1723
|
+
config: mockConfig,
|
|
1724
|
+
settings: mockSettings,
|
|
1725
|
+
input: 'Test input',
|
|
1726
|
+
prompt_id: 'prompt-id-accept-only',
|
|
1727
|
+
});
|
|
1728
|
+
expect(getWrittenOutput()).toBe(`${ANSI_SEQUENCE}\n`);
|
|
1729
|
+
});
|
|
1730
|
+
it('should warn when rawOutput is true and acceptRisk is false', async () => {
|
|
1731
|
+
const events = [
|
|
1732
|
+
{
|
|
1733
|
+
type: GeminiEventType.Finished,
|
|
1734
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
|
1735
|
+
},
|
|
1736
|
+
];
|
|
1737
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1738
|
+
vi.mocked(mockConfig.getRawOutput).mockReturnValue(true);
|
|
1739
|
+
vi.mocked(mockConfig.getAcceptRawOutputRisk).mockReturnValue(false);
|
|
1740
|
+
await runNonInteractive({
|
|
1741
|
+
config: mockConfig,
|
|
1742
|
+
settings: mockSettings,
|
|
1743
|
+
input: 'Test input',
|
|
1744
|
+
prompt_id: 'prompt-id-warn',
|
|
1745
|
+
});
|
|
1746
|
+
expect(processStderrSpy).toHaveBeenCalledWith(expect.stringContaining('[WARNING] --raw-output is enabled'));
|
|
1747
|
+
});
|
|
1748
|
+
it('should not warn when rawOutput is true and acceptRisk is true', async () => {
|
|
1749
|
+
const events = [
|
|
1750
|
+
{
|
|
1751
|
+
type: GeminiEventType.Finished,
|
|
1752
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 0 } },
|
|
1753
|
+
},
|
|
1754
|
+
];
|
|
1755
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1756
|
+
vi.mocked(mockConfig.getRawOutput).mockReturnValue(true);
|
|
1757
|
+
vi.mocked(mockConfig.getAcceptRawOutputRisk).mockReturnValue(true);
|
|
1758
|
+
await runNonInteractive({
|
|
1759
|
+
config: mockConfig,
|
|
1760
|
+
settings: mockSettings,
|
|
1761
|
+
input: 'Test input',
|
|
1762
|
+
prompt_id: 'prompt-id-no-warn',
|
|
1763
|
+
});
|
|
1764
|
+
expect(processStderrSpy).not.toHaveBeenCalledWith(expect.stringContaining('[WARNING] --raw-output is enabled'));
|
|
1765
|
+
});
|
|
1766
|
+
it('should emit warning event for loop_detected in streaming JSON mode', async () => {
|
|
1767
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.STREAM_JSON);
|
|
1768
|
+
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(MOCK_SESSION_METRICS);
|
|
1769
|
+
const streamEvents = [
|
|
1770
|
+
{ type: GeminiEventType.LoopDetected },
|
|
1771
|
+
{ type: GeminiEventType.Content, value: 'Continuing after loop' },
|
|
1772
|
+
{
|
|
1773
|
+
type: GeminiEventType.Finished,
|
|
1774
|
+
value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } },
|
|
1775
|
+
},
|
|
1776
|
+
];
|
|
1777
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(streamEvents));
|
|
1778
|
+
await runNonInteractive({
|
|
1779
|
+
config: mockConfig,
|
|
1780
|
+
settings: mockSettings,
|
|
1781
|
+
input: 'Loop test explicit',
|
|
1782
|
+
prompt_id: 'prompt-id-loop-explicit',
|
|
1783
|
+
});
|
|
1784
|
+
const output = getWrittenOutput();
|
|
1785
|
+
// The STREAM_JSON output should contain an error event with warning severity
|
|
1786
|
+
expect(output).toContain('"type":"error"');
|
|
1787
|
+
expect(output).toContain('"severity":"warning"');
|
|
1788
|
+
expect(output).toContain('Loop detected');
|
|
1789
|
+
});
|
|
1790
|
+
it('should report cancelled tool calls as success in stream-json mode (legacy parity)', async () => {
|
|
1791
|
+
const toolCallEvent = {
|
|
1792
|
+
type: GeminiEventType.ToolCallRequest,
|
|
1793
|
+
value: {
|
|
1794
|
+
callId: 'tool-1',
|
|
1795
|
+
name: 'testTool',
|
|
1796
|
+
args: { arg1: 'value1' },
|
|
1797
|
+
isClientInitiated: false,
|
|
1798
|
+
prompt_id: 'prompt-id-cancel',
|
|
1799
|
+
},
|
|
1800
|
+
};
|
|
1801
|
+
// Mock the scheduler to return a cancelled status
|
|
1802
|
+
mockSchedulerSchedule.mockResolvedValue([
|
|
1803
|
+
{
|
|
1804
|
+
status: CoreToolCallStatus.Cancelled,
|
|
1805
|
+
request: toolCallEvent.value,
|
|
1806
|
+
tool: {},
|
|
1807
|
+
invocation: {},
|
|
1808
|
+
response: {
|
|
1809
|
+
callId: 'tool-1',
|
|
1810
|
+
responseParts: [{ text: 'Operation cancelled' }],
|
|
1811
|
+
resultDisplay: 'Cancelled',
|
|
1812
|
+
},
|
|
1813
|
+
},
|
|
1814
|
+
]);
|
|
1815
|
+
const events = [
|
|
1816
|
+
toolCallEvent,
|
|
1817
|
+
{
|
|
1818
|
+
type: GeminiEventType.Content,
|
|
1819
|
+
value: 'Model continues...',
|
|
1820
|
+
},
|
|
1821
|
+
];
|
|
1822
|
+
mockGeminiClient.sendMessageStream.mockReturnValue(createStreamFromEvents(events));
|
|
1823
|
+
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.STREAM_JSON);
|
|
1824
|
+
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(MOCK_SESSION_METRICS);
|
|
1825
|
+
await runNonInteractive({
|
|
1826
|
+
config: mockConfig,
|
|
1827
|
+
settings: mockSettings,
|
|
1828
|
+
input: 'Test input',
|
|
1829
|
+
prompt_id: 'prompt-id-cancel',
|
|
1830
|
+
});
|
|
1831
|
+
const output = getWrittenOutput();
|
|
1832
|
+
expect(output).toContain('"type":"tool_result"');
|
|
1833
|
+
expect(output).toContain('"status":"success"');
|
|
1834
|
+
});
|
|
1835
|
+
});
|
|
1836
|
+
});
|
|
1837
|
+
//# sourceMappingURL=nonInteractiveCliAgentSession.test.js.map
|