Files
zk-data-agent/tests/test_cost_exit_diff.py
T
Abdelrahman Abdallah aacf0a212a Created src/prompt_constants.py (550+ lines) porting all constants from npm src/constants/:
┌──────────────────┬───────────────────────────┬───────────────────────────────────────────────┐
│ Category         │ npm Source                │ Items                                         │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Product metadata │ product.ts                │ URLs, base URLs                               │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ System prefixes  │ system.ts                 │ 3 prompt prefixes                             │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Cyber risk       │ cyberRiskInstruction.ts   │ Safety instruction                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ API limits       │ apiLimits.ts              │ 10 image/PDF/media limits                     │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Tool limits      │ toolLimits.ts             │ 6 result size constants                       │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Spinner verbs    │ spinnerVerbs.ts           │ 187 whimsical gerunds                         │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Completion verbs │ turnCompletionVerbs.ts    │ 8 past-tense verbs                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Figures/symbols  │ figures.ts                │ 25 Unicode UI symbols                         │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ XML tags         │ xml.ts                    │ 30+ tag constants                             │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Messages         │ messages.ts               │ NO_CONTENT_MESSAGE                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Date utilities   │ common.ts                 │ 4 functions                                   │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Section caching  │ systemPromptSections.ts   │ Memoized/volatile sections                    │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Output styles    │ outputStyles.ts           │ 3 built-in configs                            │
├──────────────────┼───────────────────────────┼───────────────────────────────────────────────┤
│ Prompt helpers   │ prompts.ts                │ Knowledge cutoff, language, scratchpad, hooks │
└──────────────────┴───────────────────────────┴───────────────────────────────────────────────┘

91 new tests in tests/test_prompt_constants.py. All 17 SQL todos done.
2026-04-08 00:02:53 +02:00

190 lines
7.3 KiB
Python

"""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()