Coverage for tests/test_cli.py: 98.37%

237 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2026-09-27 04:08 +0000

1from __future__ import annotations 

2 

3import dataclasses 

4import io 

5import subprocess 

6import sys 

7from collections.abc import Callable 

8from datetime import datetime, timezone 

9from pathlib import Path 

10 

11import pytest 

12from dirty_equals import IsStr 

13from inline_snapshot import snapshot 

14from rich.console import Console 

15 

16import voice_prices._cli as cli_module 

17from voice_prices import update_prices 

18from voice_prices._cli import ( 

19 _parse_cli, 

20 _render_calc_error, 

21 _should_split_model_price_columns, 

22 _suggest_models, 

23 cli_logic, 

24) 

25from voice_prices.data import providers 

26from voice_prices.types import ClauseEquals, ModelInfo, ModelPrice, Provider, TieredPrices 

27 

28 

29def _find_model_ref(predicate: Callable[[ModelPrice], bool], *, exclude: set[str] | None = None) -> str: 

30 exclude = exclude or set() 

31 now = datetime.now(timezone.utc) 

32 for provider in providers: 32 ↛ 40line 32 didn't jump to line 40 because the loop on line 32 didn't complete

33 for model in provider.models: 

34 prices = model.get_prices(now) 

35 if predicate(prices): 

36 model_ref = f'{provider.id}:{model.id}' 

37 if model_ref in exclude: 37 ↛ 38line 37 didn't jump to line 38 because the condition on line 37 was never true

38 continue 

39 return model_ref 

40 raise AssertionError('No matching model found') 

41 

42 

43def _has_tiered_prices(model_price: ModelPrice) -> bool: 

44 return any(isinstance(getattr(model_price, field.name), TieredPrices) for field in dataclasses.fields(model_price)) 

45 

46 

47def test_version(capsys: pytest.CaptureFixture[str]): 

48 assert cli_logic(['--version']) == 0 

49 out, err = capsys.readouterr() 

50 assert out == IsStr(regex=r'voice-prices .*\n') 

51 assert err == '' 

52 

53 

54def test_version_plain(capsys: pytest.CaptureFixture[str]): 

55 assert cli_logic(['--plain', '--version']) == 0 

56 out, err = capsys.readouterr() 

57 assert out == IsStr(regex=r'voice-prices .*\n') 

58 assert err == '' 

59 

60 

61def test_cli_unknown_flag(capsys: pytest.CaptureFixture[str]): 

62 assert cli_logic(['--unknown-flag']) == 2 

63 out, err = capsys.readouterr() 

64 assert out == '' 

65 assert 'unrecognized arguments: --unknown-flag' in err 

66 

67 

68def test_cli_no_subcommand_help(capsys: pytest.CaptureFixture[str]): 

69 assert cli_logic([]) == 1 

70 out, err = capsys.readouterr() 

71 assert 'usage:' in out.lower() 

72 assert 'calc' in out 

73 assert 'list' in out 

74 assert err == '' 

75 

76 

77def test_parse_cli_none(monkeypatch: pytest.MonkeyPatch): 

78 monkeypatch.setattr(cli_module.sys, 'argv', [cli_module.PROGRAM_NAME, '--version']) 

79 cli = _parse_cli(None) 

80 assert cli.version is True 

81 

82 

83def test_cli_import_exits_for_missing_optional_dependency(): 

84 package_path = Path(__file__).parents[1] / 'packages' / 'python' 

85 script_path = Path(__file__).with_name('cli_missing_dependency_import.py') 

86 

87 result = subprocess.run( 

88 [sys.executable, str(script_path), str(package_path)], capture_output=True, text=True, check=False 

89 ) 

90 assert result.returncode == 1 

91 assert result.stdout == '' 

92 assert result.stderr == ( 

93 'Optional CLI dependency \'rich\' is not installed. Install CLI extras with: pip install "voice-prices[cli]"\n' 

94 ) 

95 

96 

97def test_render_calc_error_escapes_rich_markup_in_message(): 

98 stream = io.StringIO() 

99 console = Console(file=stream, force_terminal=False, color_system=None) 

100 

101 _render_calc_error( 

102 console, 

103 message='bad [red]oops[/] message', 

104 model_ref='gpt-4o', 

105 provider_id=None, 

106 providers=[], 

107 plain=False, 

108 use_color=True, 

109 ) 

110 

111 assert '[red]oops[/]' in stream.getvalue() 

112 

113 

114def test_calc(capsys: pytest.CaptureFixture[str]): 

115 assert cli_logic(['--plain', 'calc', '--input-tokens', '1000', '--output-tokens', '100', 'gpt-4o']) == 0 

116 out, err = capsys.readouterr() 

117 assert out == snapshot("""\ 

118 Provider: OpenAI 

119 Model: gpt 4o 

120 Model Prices: $2.5/input MTok, $1.25/cache read MTok, $10/output MTok 

121Context Window: 128,000 

122 Input Price: $0.0025 

123 Output Price: $0.001 

124 Total Price: $0.0035 

125 

126""") 

127 assert err == '' 

128 

129 

130def test_calc_with_provider(capsys: pytest.CaptureFixture[str]): 

131 assert ( 

132 cli_logic( 

133 [ 

134 '--plain', 

135 'calc', 

136 '--input-tokens', 

137 '1000', 

138 '--output-tokens', 

139 '100', 

140 'azure:gpt-3.5-turbo-16k-0613', 

141 ] 

142 ) 

143 == 0 

144 ) 

145 out, err = capsys.readouterr() 

146 assert out == snapshot("""\ 

147 Provider: Microsoft Azure 

148 Model: GPT-3.5 Turbo 16k 

149 Model Prices: $3/input MTok, $4/output MTok 

150Context Window: 16,385 

151 Input Price: $0.003 

152 Output Price: $0.0004 

153 Total Price: $0.0034 

154 

155""") 

156 assert err == '' 

157 

158 

159def test_calc_timestamp(capsys: pytest.CaptureFixture[str]): 

160 assert cli_logic(['--plain', 'calc', '--input-tokens', '10000', 'o3']) == 0 

161 out, err = capsys.readouterr() 

162 assert out == snapshot("""\ 

163 Provider: OpenAI 

164 Model: o3 

165 Model Prices: $2/input MTok, $0.5/cache read MTok, $8/output MTok 

166 Input Price: $0.02 

167 Output Price: $0 

168 Total Price: $0.02 

169 

170""") 

171 assert err == '' 

172 

173 assert cli_logic(['--plain', 'calc', '--input-tokens', '10000', '--timestamp', '2025-06-01', 'o3']) == 0 

174 out, err = capsys.readouterr() 

175 assert out == snapshot("""\ 

176 Provider: OpenAI 

177 Model: o3 

178 Model Prices: $10/input MTok, $0.5/cache read MTok, $40/output MTok 

179 Input Price: $0.1 

180 Output Price: $0 

181 Total Price: $0.1 

182 

183""") 

184 assert err == '' 

185 

186 

187def test_list(capsys: pytest.CaptureFixture[str]): 

188 assert cli_logic(['--plain', 'list']) == 0 

189 out, err = capsys.readouterr() 

190 assert out.count('\n') > 100 

191 assert err == '' 

192 

193 

194def test_list_provider(capsys: pytest.CaptureFixture[str]): 

195 assert cli_logic(['--plain', 'list', 'deepseek']) == 0 

196 out, err = capsys.readouterr() 

197 assert out == snapshot("""\ 

198Deepseek: (4 models) 

199 deepseek:deepseek-chat: DeepSeek Chat 

200 deepseek:deepseek-reasoner: Deepseek R1 

201 deepseek:deepseek-v4-flash: DeepSeek V4 Flash 

202 deepseek:deepseek-v4-pro: DeepSeek V4 Pro 

203""") 

204 assert err == '' 

205 

206 

207def test_list_provider_wrong(capsys: pytest.CaptureFixture[str]): 

208 assert cli_logic(['--plain', 'list', 'foobar']) == 1 

209 out, err = capsys.readouterr() 

210 assert out == '' 

211 assert err == IsStr(regex="^Error: provider 'foobar' not found in .*\n") 

212 

213 

214def test_list_rich(capsys: pytest.CaptureFixture[str]): 

215 assert cli_logic(['list']) == 0 

216 out, err = capsys.readouterr() 

217 assert 'models' in out 

218 assert err == '' 

219 

220 

221def test_list_provider_rich(capsys: pytest.CaptureFixture[str]): 

222 assert cli_logic(['list', 'deepseek']) == 0 

223 out, err = capsys.readouterr() 

224 assert 'deepseek' in out 

225 assert err == '' 

226 

227 

228def test_list_provider_wrong_rich(capsys: pytest.CaptureFixture[str]): 

229 assert cli_logic(['list', 'foobar']) == 1 

230 out, err = capsys.readouterr() 

231 assert out == '' 

232 assert 'Error:' in err 

233 

234 

235def test_calc_table_compact(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]): 

236 monkeypatch.setenv('COLUMNS', '80') 

237 assert ( 

238 cli_logic(['calc', '--no-color', '--table', '--input-tokens', '1000', '--output-tokens', '100', 'gpt-4o']) == 0 

239 ) 

240 out, err = capsys.readouterr() 

241 assert 'Prices' in out 

242 assert 'Input/MTok' not in out 

243 assert err == '' 

244 

245 

246def test_calc_table_split_columns(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]): 

247 monkeypatch.setenv('COLUMNS', '200') 

248 assert ( 

249 cli_logic( 

250 [ 

251 'calc', 

252 '--no-color', 

253 '--table', 

254 '--input-tokens', 

255 '1000', 

256 '--output-tokens', 

257 '100', 

258 'gpt-4o', 

259 'azure:gpt-3.5-turbo-16k-0613', 

260 ] 

261 ) 

262 == 0 

263 ) 

264 out, err = capsys.readouterr() 

265 assert 'Model Prices' not in out 

266 assert 'Input/MTok' in out 

267 assert 'Cache Read/MTok' in out 

268 assert 'Cache Write/MTok' not in out 

269 assert 'Input Audio/MTok' not in out 

270 assert err == '' 

271 

272 

273def test_calc_rich_columns(capsys: pytest.CaptureFixture[str]): 

274 assert cli_logic(['calc', '--input-tokens', '1000', '--output-tokens', '100', 'gpt-4o', 'o3']) == 0 

275 out, err = capsys.readouterr() 

276 assert out.count('Provider') >= 2 

277 assert err == '' 

278 

279 

280def test_calc_no_color_vertical_table(capsys: pytest.CaptureFixture[str]): 

281 assert cli_logic(['calc', '--no-color', '--input-tokens', '1000', '--output-tokens', '100', 'gpt-4o']) == 0 

282 out, err = capsys.readouterr() 

283 assert 'Provider' in out 

284 assert 'Model Prices' in out 

285 assert err == '' 

286 

287 

288def test_calc_keep_going(capsys: pytest.CaptureFixture[str]): 

289 assert ( 

290 cli_logic( 

291 [ 

292 'calc', 

293 '--keep-going', 

294 '--no-color', 

295 '--input-tokens', 

296 '1000', 

297 '--output-tokens', 

298 '100', 

299 'gpt-4o', 

300 'does-not-exist', 

301 ] 

302 ) 

303 == 1 

304 ) 

305 out, err = capsys.readouterr() 

306 assert 'gpt 4o' in out 

307 assert 'Error:' in err 

308 

309 

310def test_calc_keep_going_all_invalid(capsys: pytest.CaptureFixture[str]): 

311 assert cli_logic(['calc', '--keep-going', '--input-tokens', '1000', 'does-not-exist']) == 1 

312 out, err = capsys.readouterr() 

313 assert out == '' 

314 assert 'Error:' in err 

315 

316 

317def test_calc_table_error_renders_summary(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]): 

318 monkeypatch.setenv('COLUMNS', '200') 

319 assert cli_logic(['calc', '--table', '--input-tokens', '1000', 'gpt-4o', 'does-not-exist']) == 1 

320 out, err = capsys.readouterr() 

321 assert 'Provider' in out 

322 assert 'Error:' in err 

323 

324 

325def test_calc_table_empty_results(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]): 

326 monkeypatch.setenv('COLUMNS', '200') 

327 assert cli_logic(['calc', '--table', '--keep-going', '--input-tokens', '1000', 'does-not-exist']) == 1 

328 out, err = capsys.readouterr() 

329 assert out == '' 

330 assert 'Error:' in err 

331 

332 

333def test_calc_provider_suggestions(capsys: pytest.CaptureFixture[str]): 

334 assert cli_logic(['--plain', 'calc', '--input-tokens', '1000', 'opnai:gpt-4o']) == 1 

335 out, err = capsys.readouterr() 

336 assert out == '' 

337 assert 'Did you mean provider: openai' in err 

338 

339 

340def test_calc_model_suggestions(capsys: pytest.CaptureFixture[str]): 

341 assert cli_logic(['--plain', 'calc', '--input-tokens', '1000', 'openai:gpt-4o0']) == 1 

342 out, err = capsys.readouterr() 

343 assert out == '' 

344 assert 'Did you mean: openai:gpt-4o' in err 

345 

346 

347def test_calc_provider_suggestions_rich_color(capsys: pytest.CaptureFixture[str]): 

348 assert cli_logic(['calc', '--input-tokens', '1000', 'opnai:gpt-4o']) == 1 

349 out, err = capsys.readouterr() 

350 assert out == '' 

351 assert 'Did you mean provider:' in err 

352 

353 

354def test_calc_provider_suggestions_rich_no_color(capsys: pytest.CaptureFixture[str]): 

355 assert cli_logic(['calc', '--no-color', '--input-tokens', '1000', 'opnai:gpt-4o']) == 1 

356 out, err = capsys.readouterr() 

357 assert out == '' 

358 assert 'Did you mean provider: openai' in err 

359 

360 

361def test_calc_model_suggestions_rich_color(capsys: pytest.CaptureFixture[str]): 

362 assert cli_logic(['calc', '--input-tokens', '1000', 'openai:gpt-4o0']) == 1 

363 out, err = capsys.readouterr() 

364 assert out == '' 

365 assert 'Did you mean:' in err 

366 

367 

368def test_calc_model_suggestions_rich_no_color(capsys: pytest.CaptureFixture[str]): 

369 assert cli_logic(['calc', '--no-color', '--input-tokens', '1000', 'openai:gpt-4o0']) == 1 

370 out, err = capsys.readouterr() 

371 assert out == '' 

372 assert 'Did you mean: openai:gpt-4o' in err 

373 

374 

375def test_calc_requests_kcount_rich(capsys: pytest.CaptureFixture[str]): 

376 model_ref = _find_model_ref(lambda price: price.requests_kcount is not None) 

377 assert cli_logic(['calc', '--input-tokens', '1000', model_ref]) == 0 

378 out, err = capsys.readouterr() 

379 assert 'K requests' in out 

380 assert err == '' 

381 

382 

383def test_calc_table_split_columns_rich_prices(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]): 

384 tiered_ref = _find_model_ref(_has_tiered_prices) 

385 requests_ref = _find_model_ref( 

386 lambda price: price.requests_kcount is not None, 

387 exclude={tiered_ref}, 

388 ) 

389 monkeypatch.setenv('COLUMNS', '200') 

390 assert cli_logic(['calc', '--table', '--input-tokens', '1000', tiered_ref, requests_ref]) == 0 

391 out, err = capsys.readouterr() 

392 assert '(+tiers)' in out 

393 assert 'Requests/K' in out 

394 assert err == '' 

395 

396 

397def test_split_model_price_columns_no_fields(): 

398 console = Console(width=60) 

399 assert _should_split_model_price_columns(console, []) is False 

400 

401 

402def test_suggest_models_missing_provider(): 

403 assert _suggest_models('gpt-4o', 'missing', providers) == [] 

404 

405 

406def test_suggest_models_case_insensitive_provider_path(): 

407 model_id = 'AaAaAaAaAaAaAaAaAaAa' 

408 fake_providers = [ 

409 Provider( 

410 id='provider', 

411 name='Provider', 

412 api_pattern='https://example.com', 

413 models=[ModelInfo(id=model_id, match=ClauseEquals(model_id))], 

414 ) 

415 ] 

416 assert _suggest_models(model_id.lower(), 'provider', fake_providers) == [f'provider:{model_id}'] 

417 

418 

419def test_suggest_models_case_insensitive_global_path(): 

420 model_id = 'AaAaAaAaAaAaAaAaAaAa' 

421 fake_providers = [ 

422 Provider( 

423 id='provider', 

424 name='Provider', 

425 api_pattern='https://example.com', 

426 models=[ModelInfo(id=model_id, match=ClauseEquals(model_id))], 

427 ) 

428 ] 

429 assert _suggest_models(f'provider:{model_id.lower()}', None, fake_providers) == [f'provider:{model_id}'] 

430 

431 

432def test_calc_update_prices(monkeypatch: pytest.MonkeyPatch): 

433 calls: dict[str, int | bool] = {'instances': 0, 'starts': 0} 

434 

435 class DummyUpdatePrices: 

436 def __init__(self) -> None: 

437 calls['instances'] += 1 

438 

439 def start(self, *, wait: bool = False) -> None: 

440 calls['starts'] += 1 

441 calls['wait'] = wait 

442 

443 monkeypatch.setattr(update_prices, 'UpdatePrices', DummyUpdatePrices) 

444 assert cli_logic(['--plain', 'calc', '--update-prices', '--input-tokens', '1000', 'gpt-4o', 'gpt-4o']) == 0 

445 assert calls == {'instances': 1, 'starts': 1, 'wait': True}