"""Tests for /cost, /exit, and /diff slash commands.""" from __future__ import annotations import subprocess import tempfile import unittest from pathlib import Path from unittest.mock import MagicMock, patch from src.agent_runtime import LocalCodingAgent from src.agent_slash_commands import preprocess_slash_command from src.agent_types import AgentRuntimeConfig, ModelConfig, UsageStats class TestCostCommand(unittest.TestCase): """Tests for the /cost slash command.""" def test_cost_shows_zero_initially(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: agent = LocalCodingAgent( model_config=ModelConfig(model='test-model'), runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)), ) result = agent.run('/cost') self.assertIn('Total cost:', result.final_output) self.assertIn('$0.0000', result.final_output) self.assertIn('Total input tokens:', result.final_output) self.assertIn('Total output tokens:', result.final_output) self.assertIn('Total tokens:', result.final_output) def test_cost_shows_accumulated_usage(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: agent = LocalCodingAgent( model_config=ModelConfig(model='test-model'), runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)), ) # Simulate accumulated usage agent.cumulative_usage = UsageStats( input_tokens=1000, output_tokens=500, cache_read_input_tokens=200, ) agent.cumulative_cost_usd = 0.05 result = agent.run('/cost') self.assertIn('$0.05', result.final_output) self.assertIn('1,000', result.final_output) self.assertIn('500', result.final_output) self.assertIn('Cache read tokens:', result.final_output) def test_cost_hides_zero_cache_and_reasoning(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: agent = LocalCodingAgent( model_config=ModelConfig(model='test-model'), runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)), ) agent.cumulative_usage = UsageStats( input_tokens=100, output_tokens=50, ) result = agent.run('/cost') # Should NOT show cache/reasoning lines when they're zero self.assertNotIn('Cache read tokens:', result.final_output) self.assertNotIn('Cache creation tokens:', result.final_output) self.assertNotIn('Reasoning tokens:', result.final_output) def test_cost_small_amounts_show_four_decimals(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: agent = LocalCodingAgent( model_config=ModelConfig(model='test-model'), runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)), ) agent.cumulative_cost_usd = 0.0023 result = agent.run('/cost') self.assertIn('$0.0023', result.final_output) class TestExitCommand(unittest.TestCase): """Tests for the /exit and /quit slash commands.""" def test_exit_triggers_system_exit(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: agent = LocalCodingAgent( model_config=ModelConfig(model='test-model'), runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)), ) with self.assertRaises(SystemExit) as cm: agent.run('/exit') self.assertEqual(cm.exception.code, 0) def test_quit_triggers_system_exit(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: agent = LocalCodingAgent( model_config=ModelConfig(model='test-model'), runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)), ) with self.assertRaises(SystemExit): agent.run('/quit') class TestDiffCommand(unittest.TestCase): """Tests for the /diff slash command.""" def test_diff_in_non_git_directory(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: agent = LocalCodingAgent( model_config=ModelConfig(model='test-model'), runtime_config=AgentRuntimeConfig(cwd=Path(tmp_dir)), ) result = agent.run('/diff') # In a non-git dir, git diff returns an error output = result.final_output.lower() self.assertTrue( 'no uncommitted' in output or 'not a git' in output or 'error' in output, f'Expected a non-git or no-changes message, got: {result.final_output}', ) def test_diff_in_git_repo_with_no_changes(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: workspace = Path(tmp_dir) subprocess.run( ['git', 'init'], cwd=str(workspace), capture_output=True, check=True, ) subprocess.run( ['git', 'config', 'user.email', 'test@test.com'], cwd=str(workspace), capture_output=True, ) subprocess.run( ['git', 'config', 'user.name', 'Test'], cwd=str(workspace), capture_output=True, ) (workspace / 'hello.txt').write_text('hello\n') subprocess.run( ['git', 'add', '.'], cwd=str(workspace), capture_output=True, check=True, ) subprocess.run( ['git', 'commit', '-m', 'init'], cwd=str(workspace), capture_output=True, check=True, ) agent = LocalCodingAgent( model_config=ModelConfig(model='test-model'), runtime_config=AgentRuntimeConfig(cwd=workspace), ) result = agent.run('/diff') self.assertIn('No uncommitted changes', result.final_output) def test_diff_shows_changes(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: workspace = Path(tmp_dir) subprocess.run( ['git', 'init'], cwd=str(workspace), capture_output=True, check=True, ) subprocess.run( ['git', 'config', 'user.email', 'test@test.com'], cwd=str(workspace), capture_output=True, ) subprocess.run( ['git', 'config', 'user.name', 'Test'], cwd=str(workspace), capture_output=True, ) (workspace / 'hello.txt').write_text('hello\n') subprocess.run( ['git', 'add', '.'], cwd=str(workspace), capture_output=True, check=True, ) subprocess.run( ['git', 'commit', '-m', 'init'], cwd=str(workspace), capture_output=True, check=True, ) # Make a change (workspace / 'hello.txt').write_text('hello world\n') agent = LocalCodingAgent( model_config=ModelConfig(model='test-model'), runtime_config=AgentRuntimeConfig(cwd=workspace), ) result = agent.run('/diff') self.assertIn('hello', result.final_output) self.assertIn('diff', result.final_output.lower()) if __name__ == '__main__': unittest.main()