planar 0.9.3__py3-none-any.whl → 0.11.0__py3-none-any.whl

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.
Files changed (76) hide show
  1. planar/ai/agent.py +2 -1
  2. planar/ai/agent_base.py +24 -5
  3. planar/ai/state.py +17 -0
  4. planar/app.py +18 -1
  5. planar/data/connection.py +108 -0
  6. planar/data/dataset.py +11 -104
  7. planar/data/utils.py +89 -0
  8. planar/db/alembic/env.py +25 -1
  9. planar/files/storage/azure_blob.py +1 -1
  10. planar/registry_items.py +2 -0
  11. planar/routers/dataset_router.py +213 -0
  12. planar/routers/info.py +79 -36
  13. planar/routers/models.py +1 -0
  14. planar/routers/workflow.py +2 -0
  15. planar/scaffold_templates/pyproject.toml.j2 +1 -1
  16. planar/security/authorization.py +31 -3
  17. planar/security/default_policies.cedar +25 -0
  18. planar/testing/fixtures.py +34 -1
  19. planar/testing/planar_test_client.py +1 -1
  20. planar/workflows/decorators.py +2 -1
  21. planar/workflows/wrappers.py +1 -0
  22. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/METADATA +9 -1
  23. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/RECORD +25 -72
  24. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/WHEEL +1 -1
  25. planar/ai/test_agent_serialization.py +0 -229
  26. planar/ai/test_agent_tool_step_display.py +0 -78
  27. planar/data/test_dataset.py +0 -354
  28. planar/files/storage/test_azure_blob.py +0 -435
  29. planar/files/storage/test_local_directory.py +0 -162
  30. planar/files/storage/test_s3.py +0 -299
  31. planar/files/test_files.py +0 -282
  32. planar/human/test_human.py +0 -385
  33. planar/logging/test_formatter.py +0 -327
  34. planar/modeling/mixins/test_auditable.py +0 -97
  35. planar/modeling/mixins/test_timestamp.py +0 -134
  36. planar/modeling/mixins/test_uuid_primary_key.py +0 -52
  37. planar/routers/test_agents_router.py +0 -174
  38. planar/routers/test_files_router.py +0 -49
  39. planar/routers/test_object_config_router.py +0 -367
  40. planar/routers/test_routes_security.py +0 -168
  41. planar/routers/test_rule_router.py +0 -470
  42. planar/routers/test_workflow_router.py +0 -539
  43. planar/rules/test_data/account_dormancy_management.json +0 -223
  44. planar/rules/test_data/airline_loyalty_points_calculator.json +0 -262
  45. planar/rules/test_data/applicant_risk_assessment.json +0 -435
  46. planar/rules/test_data/booking_fraud_detection.json +0 -407
  47. planar/rules/test_data/cellular_data_rollover_system.json +0 -258
  48. planar/rules/test_data/clinical_trial_eligibility_screener.json +0 -437
  49. planar/rules/test_data/customer_lifetime_value.json +0 -143
  50. planar/rules/test_data/import_duties_calculator.json +0 -289
  51. planar/rules/test_data/insurance_prior_authorization.json +0 -443
  52. planar/rules/test_data/online_check_in_eligibility_system.json +0 -254
  53. planar/rules/test_data/order_consolidation_system.json +0 -375
  54. planar/rules/test_data/portfolio_risk_monitor.json +0 -471
  55. planar/rules/test_data/supply_chain_risk.json +0 -253
  56. planar/rules/test_data/warehouse_cross_docking.json +0 -237
  57. planar/rules/test_rules.py +0 -1494
  58. planar/security/tests/test_auth_middleware.py +0 -162
  59. planar/security/tests/test_authorization_context.py +0 -78
  60. planar/security/tests/test_cedar_basics.py +0 -41
  61. planar/security/tests/test_cedar_policies.py +0 -158
  62. planar/security/tests/test_jwt_principal_context.py +0 -179
  63. planar/test_app.py +0 -142
  64. planar/test_cli.py +0 -394
  65. planar/test_config.py +0 -515
  66. planar/test_object_config.py +0 -527
  67. planar/test_object_registry.py +0 -14
  68. planar/test_sqlalchemy.py +0 -193
  69. planar/test_utils.py +0 -105
  70. planar/testing/test_memory_storage.py +0 -143
  71. planar/workflows/test_concurrency_detection.py +0 -120
  72. planar/workflows/test_lock_timeout.py +0 -140
  73. planar/workflows/test_serialization.py +0 -1203
  74. planar/workflows/test_suspend_deserialization.py +0 -231
  75. planar/workflows/test_workflow.py +0 -2005
  76. {planar-0.9.3.dist-info → planar-0.11.0.dist-info}/entry_points.txt +0 -0
planar/test_app.py DELETED
@@ -1,142 +0,0 @@
1
- from unittest.mock import Mock, patch
2
-
3
- import pytest
4
- from dotenv import load_dotenv
5
- from fastapi import APIRouter
6
- from pydantic import BaseModel, ValidationError
7
-
8
- from examples.simple_service.models import (
9
- Invoice,
10
- )
11
- from planar import PlanarApp, sqlite_config
12
- from planar.app import setup_auth_middleware
13
- from planar.config import Environment, JWTConfig, SecurityConfig
14
-
15
- load_dotenv()
16
-
17
-
18
- router = APIRouter()
19
-
20
-
21
- class InvoiceRequest(BaseModel):
22
- message: str
23
-
24
-
25
- class InvoiceResponse(BaseModel):
26
- status: str
27
- echo: str
28
-
29
-
30
- app = PlanarApp(
31
- config=sqlite_config("simple_service.db"),
32
- title="Sample Invoice API",
33
- description="API for CRUD'ing invoices",
34
- )
35
-
36
-
37
- def test_register_model_deduplication():
38
- """Test that registering the same model multiple times only adds it once to the registry."""
39
-
40
- # Ensure Invoice is registered (ObjectRegistry gets reset before each test)
41
- app.register_entity(Invoice)
42
- initial_model_count = len(app._object_registry.get_entities())
43
-
44
- # Register the Invoice model again
45
- app.register_entity(Invoice)
46
-
47
- assert len(app._object_registry.get_entities()) == initial_model_count
48
-
49
- # Register the same model a second time
50
- app.register_entity(Invoice)
51
-
52
- assert len(app._object_registry.get_entities()) == initial_model_count
53
-
54
- # Verify that the model in the registry is the Invoice model
55
- registered_models = app._object_registry.get_entities()
56
- assert any(model.__name__ == "Invoice" for model in registered_models)
57
-
58
-
59
- class TestJWTSetup:
60
- def test_setup_jwt_middleware_production_requires_config(self):
61
- """Test that JWT setup throws ValueError in production without proper config"""
62
- mock_app = Mock()
63
- mock_app.config.environment = Environment.PROD
64
- mock_app.config.security = SecurityConfig()
65
-
66
- with pytest.raises(
67
- ValueError,
68
- match="Auth middleware is required in production. Please set the JWT config and optionally service token config.",
69
- ):
70
- setup_auth_middleware(mock_app)
71
-
72
- def test_setup_jwt_middleware_production_requires_client_id(self):
73
- """Test that JWT setup throws ValueError in production without client_id"""
74
- with pytest.raises(
75
- ValidationError,
76
- match="Both client_id and org_id required to enable JWT",
77
- ):
78
- JWTConfig(client_id=None, org_id="test-org")
79
-
80
- def test_setup_jwt_middleware_production_requires_org_id(self):
81
- """Test that JWT setup throws ValueError in production without org_id"""
82
- with pytest.raises(
83
- ValidationError,
84
- match="Both client_id and org_id required to enable JWT",
85
- ):
86
- JWTConfig(client_id="test-client-id", org_id=None)
87
-
88
- @patch("planar.app.logger")
89
- def test_setup_jwt_middleware_success_with_all_fields(self, mock_logger):
90
- """Test that JWT setup succeeds with all required fields"""
91
- mock_app = Mock()
92
- mock_app.config.environment = Environment.PROD
93
- mock_app.config.security = SecurityConfig(
94
- jwt=JWTConfig(
95
- client_id="test-client-id",
96
- org_id="test-org-id",
97
- additional_exclusion_paths=["/test/path"],
98
- )
99
- )
100
-
101
- setup_auth_middleware(mock_app)
102
-
103
- # Verify middleware was added to app.fastapi
104
- mock_app.fastapi.add_middleware.assert_called_once()
105
-
106
- # Check that info log was called
107
- mock_logger.info.assert_called_once_with(
108
- "Auth middleware enabled",
109
- client_id="test-client-id",
110
- org_id="test-org-id",
111
- additional_exclusion_paths=["/test/path"],
112
- )
113
-
114
- @patch("planar.app.logger")
115
- def test_setup_jwt_middleware_dev_environment_allows_missing_config(
116
- self, mock_logger
117
- ):
118
- """Test that JWT setup is skipped in dev environment without config"""
119
- mock_app = Mock()
120
- mock_app.config.environment = Environment.DEV
121
- mock_app.config.security = SecurityConfig()
122
-
123
- setup_auth_middleware(mock_app)
124
-
125
- # Verify warning was logged and no middleware added
126
- mock_logger.warning.assert_called_once_with("Auth middleware disabled")
127
- mock_app.fastapi.add_middleware.assert_not_called()
128
-
129
- @patch("planar.app.logger")
130
- def test_setup_jwt_middleware_dev_environment_allows_disabled_jwt(
131
- self, mock_logger
132
- ):
133
- """Test that JWT setup is skipped in dev environment with disabled JWT"""
134
- mock_app = Mock()
135
- mock_app.config.environment = Environment.DEV
136
- mock_app.config.security = SecurityConfig()
137
-
138
- setup_auth_middleware(mock_app)
139
-
140
- # Verify warning was logged and no middleware added
141
- mock_logger.warning.assert_called_once_with("Auth middleware disabled")
142
- mock_app.fastapi.add_middleware.assert_not_called()
planar/test_cli.py DELETED
@@ -1,394 +0,0 @@
1
- from pathlib import Path
2
- from unittest.mock import patch
3
-
4
- import pytest
5
- import typer
6
- from typer.testing import CliRunner
7
-
8
- from planar.cli import app, find_default_app_path, get_module_str_from_path
9
-
10
-
11
- @pytest.fixture
12
- def cli_runner():
13
- return CliRunner()
14
-
15
-
16
- @pytest.fixture
17
- def mock_uvicorn_run():
18
- with patch("planar.cli.uvicorn.run") as mock_run:
19
- yield mock_run
20
-
21
-
22
- @pytest.fixture
23
- def mock_path_exists():
24
- with patch("pathlib.Path.exists", return_value=True) as mock_exists:
25
- yield mock_exists
26
-
27
-
28
- @pytest.fixture
29
- def mock_path_is_file():
30
- with patch("pathlib.Path.is_file", return_value=True) as mock_is_file:
31
- yield mock_is_file
32
-
33
-
34
- class TestDevCommand:
35
- def test_dev_command_defaults(
36
- self, cli_runner, mock_uvicorn_run, mock_path_is_file
37
- ):
38
- """Test that 'planar dev' sets correct defaults and invokes uvicorn.run."""
39
- env = {}
40
- with patch("os.environ", env):
41
- result = cli_runner.invoke(app, ["dev"])
42
-
43
- assert result.exit_code == 0
44
- mock_uvicorn_run.assert_called_once()
45
-
46
- # Verify uvicorn.run arguments
47
- call_args = mock_uvicorn_run.call_args
48
- assert call_args[0][0] == "app:app"
49
- assert call_args.kwargs["reload"] is True
50
- assert call_args.kwargs["host"] == "127.0.0.1"
51
- assert call_args.kwargs["port"] == 8000
52
- assert call_args.kwargs["timeout_graceful_shutdown"] == 4
53
-
54
- # Verify environment variable
55
- assert env["PLANAR_ENV"] == "dev"
56
-
57
-
58
- class TestProdCommand:
59
- def test_prod_command_defaults(
60
- self, cli_runner, mock_uvicorn_run, mock_path_is_file
61
- ):
62
- """Test that 'planar prod' sets correct defaults and invokes uvicorn.run."""
63
- env = {}
64
- with patch("os.environ", env):
65
- result = cli_runner.invoke(app, ["prod"])
66
-
67
- assert result.exit_code == 0
68
- mock_uvicorn_run.assert_called_once()
69
-
70
- # Verify uvicorn.run arguments
71
- call_args = mock_uvicorn_run.call_args
72
- assert call_args[0][0] == "app:app"
73
- assert call_args.kwargs["reload"] is False
74
- assert call_args.kwargs["host"] == "0.0.0.0"
75
- assert call_args.kwargs["port"] == 8000
76
- assert call_args.kwargs["timeout_graceful_shutdown"] == 4
77
-
78
- # Verify environment variable
79
- assert env["PLANAR_ENV"] == "prod"
80
-
81
-
82
- class TestArgumentParsing:
83
- @pytest.mark.parametrize("command", ["dev", "prod"])
84
- def test_custom_port(
85
- self, cli_runner, mock_uvicorn_run, mock_path_is_file, command
86
- ):
87
- """Test custom port settings for both dev and prod commands."""
88
- port = "9999"
89
- with patch("os.environ", {}):
90
- result = cli_runner.invoke(app, [command, "--port", port])
91
-
92
- assert result.exit_code == 0
93
- mock_uvicorn_run.assert_called_once()
94
-
95
- # Verify config arguments
96
- call_args = mock_uvicorn_run.call_args
97
- assert call_args.kwargs["port"] == int(port)
98
-
99
- @pytest.mark.parametrize("command", ["dev", "prod"])
100
- def test_custom_host(
101
- self, cli_runner, mock_uvicorn_run, mock_path_is_file, command
102
- ):
103
- """Test custom host settings."""
104
- host = "0.0.0.0"
105
- with patch("os.environ", {}):
106
- result = cli_runner.invoke(app, [command, "--host", host])
107
-
108
- assert result.exit_code == 0
109
- mock_uvicorn_run.assert_called_once()
110
-
111
- # Verify config arguments
112
- call_args = mock_uvicorn_run.call_args
113
- assert call_args.kwargs["host"] == host
114
-
115
- @pytest.mark.parametrize("command", ["dev", "prod"])
116
- def test_custom_app_name(
117
- self, cli_runner, mock_uvicorn_run, mock_path_is_file, command
118
- ):
119
- """Test custom app instance name."""
120
- with (
121
- patch("os.environ", {}),
122
- patch("planar.cli.get_module_str_from_path", return_value="app"),
123
- ):
124
- result = cli_runner.invoke(app, [command, "--app", "server"])
125
-
126
- assert result.exit_code == 0
127
- mock_uvicorn_run.assert_called_once()
128
-
129
- # Verify config arguments
130
- call_args = mock_uvicorn_run.call_args
131
- assert call_args[0][0] == "app:server"
132
-
133
- @pytest.mark.parametrize("command", ["dev", "prod"])
134
- def test_custom_path(
135
- self, cli_runner, mock_uvicorn_run, mock_path_is_file, command
136
- ):
137
- """Test custom application path."""
138
- path = "custom/app.py"
139
- with (
140
- patch("os.environ", {}),
141
- patch("planar.cli.get_module_str_from_path", return_value="custom.app"),
142
- ):
143
- result = cli_runner.invoke(app, [command, path])
144
-
145
- assert result.exit_code == 0
146
- mock_uvicorn_run.assert_called_once()
147
-
148
- # Verify config arguments
149
- call_args = mock_uvicorn_run.call_args
150
- assert call_args[0][0] == "custom.app:app"
151
-
152
- @pytest.mark.parametrize("command", ["dev", "prod"])
153
- def test_invalid_path(self, cli_runner, mock_uvicorn_run, command):
154
- """Test handling of invalid path."""
155
- with patch("pathlib.Path.is_file", return_value=False):
156
- result = cli_runner.invoke(app, [command, "nonexistent/app.py"])
157
-
158
- assert result.exit_code == 1
159
- assert "not found" in result.output
160
- mock_uvicorn_run.assert_not_called()
161
-
162
-
163
- class TestAppPathResolution:
164
- @pytest.mark.parametrize("command", ["dev", "prod"])
165
- def test_explicit_path(
166
- self, cli_runner, mock_uvicorn_run, mock_path_is_file, command
167
- ):
168
- """Test command --path sets PLANAR_ENTRY_POINT and generates correct import string."""
169
- env = {}
170
- with (
171
- patch("os.environ", env),
172
- patch("planar.cli.get_module_str_from_path", return_value="path.to.app"),
173
- ):
174
- result = cli_runner.invoke(app, [command, "path/to/app.py"])
175
-
176
- assert result.exit_code == 0
177
- assert env["PLANAR_ENTRY_POINT"] == "path/to/app.py"
178
- mock_uvicorn_run.assert_called_once()
179
-
180
- # Verify config arguments
181
- call_args = mock_uvicorn_run.call_args
182
- assert call_args[0][0] == "path.to.app:app"
183
-
184
- @pytest.mark.parametrize(
185
- "path,error_text",
186
- [
187
- ("non_existent/app.py", "not found or is not a file"),
188
- ("directory/", "not found or is not a file"),
189
- ],
190
- )
191
- def test_invalid_paths(self, cli_runner, path, error_text):
192
- """Test invalid paths exit with error."""
193
- with patch("pathlib.Path.is_file", return_value=False):
194
- result = cli_runner.invoke(app, ["dev", path])
195
-
196
- assert result.exit_code == 1
197
- assert error_text in result.output
198
-
199
-
200
- class TestDefaultPathDiscovery:
201
- @pytest.mark.parametrize(
202
- "file_exists,expected_path",
203
- [
204
- (lambda p: p.name == "app.py", Path("app.py")),
205
- (lambda p: p.name == "main.py", Path("main.py")),
206
- ],
207
- )
208
- def test_default_files(
209
- self, cli_runner, mock_uvicorn_run, file_exists, expected_path
210
- ):
211
- """Test default file discovery works for app.py and main.py."""
212
- env = {}
213
- expected_name = expected_path.stem
214
- with (
215
- patch("os.environ", env),
216
- patch("pathlib.Path.is_file", file_exists),
217
- patch("planar.cli.get_module_str_from_path", return_value=expected_name),
218
- ):
219
- result = cli_runner.invoke(app, ["dev"])
220
-
221
- assert result.exit_code == 0
222
- assert env["PLANAR_ENTRY_POINT"] == str(expected_path)
223
- mock_uvicorn_run.assert_called_once()
224
-
225
- def test_no_default_files(self, cli_runner):
226
- """Test with neither app.py nor main.py existing: Verify exit code 1 and error message."""
227
- with patch("pathlib.Path.is_file", return_value=False):
228
- result = cli_runner.invoke(app, ["dev"])
229
-
230
- assert result.exit_code == 1
231
- assert "Could not find app.py or main.py" in result.output
232
-
233
-
234
- class TestModuleStringConversion:
235
- @pytest.mark.parametrize(
236
- "path,expected",
237
- [
238
- (Path("app.py"), "app"),
239
- (Path("src/api/main.py"), "src.api.main"),
240
- ],
241
- )
242
- def test_valid_paths(self, path, expected):
243
- """Test module string conversion for valid paths."""
244
- abs_path = Path.cwd() / path
245
- with (
246
- patch("pathlib.Path.resolve", return_value=abs_path),
247
- patch("pathlib.Path.relative_to", return_value=path),
248
- ):
249
- result = get_module_str_from_path(path)
250
-
251
- assert result == expected
252
-
253
- def test_outside_cwd(self):
254
- """Test conversion for a path outside CWD results in exit code 1 and error."""
255
- path = Path("/tmp/app.py")
256
- with (
257
- patch("pathlib.Path.resolve", return_value=Path("/tmp/app.py")),
258
- patch("pathlib.Path.relative_to", side_effect=ValueError("Not relative")),
259
- ):
260
- with pytest.raises(typer.Exit) as exc_info:
261
- get_module_str_from_path(path)
262
-
263
- assert exc_info.value.exit_code == 1
264
-
265
-
266
- class TestConfigFileHandling:
267
- def test_config_handling(self, cli_runner, mock_uvicorn_run, mock_path_is_file):
268
- """Test config file handling."""
269
- env = {}
270
- with patch("os.environ", env):
271
- with patch("pathlib.Path.exists", return_value=True):
272
- result = cli_runner.invoke(
273
- app, ["dev", "--config", "valid/config.yaml"]
274
- )
275
-
276
- assert result.exit_code == 0
277
- assert env["PLANAR_CONFIG"] == "valid/config.yaml"
278
- mock_uvicorn_run.assert_called_once()
279
-
280
- # Test invalid config
281
- with patch("pathlib.Path.exists", return_value=False):
282
- result = cli_runner.invoke(app, ["dev", "--config", "invalid/config.yaml"])
283
-
284
- assert result.exit_code == 1
285
- assert "Config file invalid/config.yaml not found" in result.output
286
-
287
-
288
- class TestUvicornInteraction:
289
- def test_successful_run(self, cli_runner, mock_uvicorn_run, mock_path_is_file):
290
- """Test successful uvicorn.run with correct parameters."""
291
- with (
292
- patch("os.environ", {}),
293
- patch("planar.cli.get_module_str_from_path", return_value="src.main"),
294
- ):
295
- result = cli_runner.invoke(app, ["dev", "src/main.py"])
296
-
297
- assert result.exit_code == 0
298
- mock_uvicorn_run.assert_called_once()
299
-
300
- # Verify config arguments
301
- call_args = mock_uvicorn_run.call_args
302
- assert call_args[0][0] == "src.main:app"
303
- assert call_args.kwargs["host"] == "127.0.0.1"
304
- assert call_args.kwargs["port"] == 8000
305
- assert call_args.kwargs["reload"] is True
306
-
307
-
308
- class TestScaffoldCommand:
309
- def test_scaffold_creates_project_structure(self, cli_runner, tmp_path):
310
- """Test that scaffold command creates the correct directory structure and files."""
311
- project_name = "test_project"
312
-
313
- result = cli_runner.invoke(
314
- app, ["scaffold", "--name", project_name, "--directory", str(tmp_path)]
315
- )
316
-
317
- assert result.exit_code == 0
318
- assert "created successfully" in result.output
319
-
320
- # Check project directory exists
321
- project_dir = tmp_path / project_name
322
- assert project_dir.exists()
323
- assert project_dir.is_dir()
324
-
325
- # Check directory structure
326
- assert (project_dir / "app").exists()
327
- assert (project_dir / "app" / "db").exists()
328
- assert (project_dir / "app" / "flows").exists()
329
-
330
- # Check expected files exist
331
- expected_files = [
332
- "app/__init__.py",
333
- "app/db/entities.py",
334
- "app/flows/process_invoice.py",
335
- "main.py",
336
- "pyproject.toml",
337
- "planar.dev.yaml",
338
- "planar.prod.yaml",
339
- ]
340
-
341
- for file_path in expected_files:
342
- assert (project_dir / file_path).exists(), f"File {file_path} should exist"
343
- assert (project_dir / file_path).is_file(), f"{file_path} should be a file"
344
-
345
- def test_scaffold_fails_if_directory_exists(self, cli_runner, tmp_path):
346
- """Test that scaffold command fails if target directory already exists."""
347
- project_name = "existing_project"
348
- existing_dir = tmp_path / project_name
349
- existing_dir.mkdir()
350
-
351
- result = cli_runner.invoke(
352
- app, ["scaffold", "--name", project_name, "--directory", str(tmp_path)]
353
- )
354
-
355
- assert result.exit_code == 1
356
- assert "already exists" in result.output
357
-
358
- def test_scaffold_with_default_directory(self, cli_runner, tmp_path, monkeypatch):
359
- """Test scaffold command with default directory (current directory)."""
360
- project_name = "test_project"
361
-
362
- # Change to tmp_path to test default directory behavior
363
- monkeypatch.chdir(tmp_path)
364
-
365
- result = cli_runner.invoke(app, ["scaffold", "--name", project_name])
366
-
367
- assert result.exit_code == 0
368
-
369
- # Check project directory exists in current directory
370
- project_dir = tmp_path / project_name
371
- assert project_dir.exists()
372
- assert (project_dir / "main.py").exists()
373
-
374
-
375
- class TestUtilityFunctions:
376
- @pytest.mark.parametrize(
377
- "file_exists,expected",
378
- [
379
- (lambda p: p.name == "app.py", Path("app.py")),
380
- (lambda p: p.name == "main.py", Path("main.py")),
381
- ],
382
- )
383
- def test_find_default_app_path(self, file_exists, expected):
384
- """Test find_default_app_path function."""
385
- with patch("pathlib.Path.is_file", file_exists):
386
- path = find_default_app_path()
387
- assert path == expected
388
-
389
- def test_find_default_app_path_no_files(self):
390
- """Test find_default_app_path raises typer.Exit when no default files exist."""
391
- with patch("pathlib.Path.is_file", return_value=False):
392
- with pytest.raises(typer.Exit) as exc_info:
393
- find_default_app_path()
394
- assert exc_info.value.exit_code == 1