@@ -280,16 +280,16 @@ def test_deploy_pyproject_error_message_mentions_quickstart(runner: CliRunner, p
280280]
281281
282282
283- @ pytest .mark . parametrize ( "app_mode,entrypoint,expected_builder_name" , DISPATCH_MATRIX )
284- def test_deploy_pyproject_dispatches_by_app_mode (
285- runner : CliRunner ,
286- project_dir : pathlib . Path ,
287- app_mode : str ,
288- entrypoint : str ,
289- expected_builder_name : str ,
290- monkeypatch : pytest . MonkeyPatch ,
291- ):
292- """Each ``[tool.rsconnect].app_mode`` routes to its matching bundle builder."""
283+ def _spy_make_bundle ( monkeypatch : pytest .MonkeyPatch ) -> dict [ str , typing . Any ]:
284+ """Short-circuit a deploy at ``make_bundle`` and capture the dispatched call.
285+
286+ Records the builder name plus the positional/keyword args passed to
287+ ``make_bundle`` (the bundle builder is ``args[0]`` to ``make_bundle`` and the
288+ entrypoint is ``args[1]`` of the captured ``args``), then raises a sentinel so
289+ no network call happens.
290+
291+ :param monkeypatch: pytest fixture used to stub out the deploy collaborators.
292+ """
293293 captured : dict [str , typing .Any ] = {}
294294
295295 class _StopDispatch (Exception ):
@@ -319,6 +319,20 @@ def spy_make_bundle(
319319 monkeypatch .setattr (api_mod .RSConnectExecutor , "validate_server" , lambda self : self )
320320 monkeypatch .setattr (api_mod .RSConnectExecutor , "validate_app_mode" , lambda self , app_mode : self )
321321 monkeypatch .setattr (api_mod .RSConnectExecutor , "make_bundle" , spy_make_bundle )
322+ return captured
323+
324+
325+ @pytest .mark .parametrize ("app_mode,entrypoint,expected_builder_name" , DISPATCH_MATRIX )
326+ def test_deploy_pyproject_dispatches_by_app_mode (
327+ runner : CliRunner ,
328+ project_dir : pathlib .Path ,
329+ app_mode : str ,
330+ entrypoint : str ,
331+ expected_builder_name : str ,
332+ monkeypatch : pytest .MonkeyPatch ,
333+ ):
334+ """Each ``[tool.rsconnect].app_mode`` routes to its matching bundle builder."""
335+ captured = _spy_make_bundle (monkeypatch )
322336
323337 _write_pyproject (
324338 project_dir ,
@@ -347,6 +361,47 @@ def spy_make_bundle(
347361 pass
348362
349363
364+ _EXPRESS_APP = "from shiny.express import ui\n \n ui.h1('hi')\n "
365+
366+
367+ def test_deploy_pyproject_shiny_express_entrypoint_matches_deploy_shiny (
368+ runner : CliRunner , project_dir : pathlib .Path , monkeypatch : pytest .MonkeyPatch
369+ ):
370+ """A Shiny Express app deployed via ``deploy pyproject`` gets the same
371+ ``shiny.express.app:`` entrypoint as the dedicated ``deploy shiny`` path.
372+
373+ Without the rewrite Connect cannot find an application object and the app
374+ crashes on boot, so both deploy paths must agree on the express form.
375+ """
376+ (project_dir / "app.py" ).write_text (_EXPRESS_APP )
377+ _write_pyproject (
378+ project_dir ,
379+ """
380+ [project]
381+ name = "hello_app"
382+ version = "0.0.1"
383+ dependencies = ["shiny"]
384+
385+ [tool.rsconnect]
386+ app_mode = "python-shiny"
387+ entrypoint = "app.py"
388+ title = "Express App"
389+ """ ,
390+ )
391+ server = ["-s" , "http://example.invalid" , "-k" , "fake-key" ]
392+
393+ captured = _spy_make_bundle (monkeypatch )
394+ runner .invoke (cli , ["deploy" , "shiny" , str (project_dir ), * server ])
395+ shiny_entrypoint = captured ["args" ][1 ]
396+
397+ captured = _spy_make_bundle (monkeypatch )
398+ runner .invoke (cli , ["deploy" , "pyproject" , str (project_dir ), * server ])
399+ pyproject_entrypoint = captured ["args" ][1 ]
400+
401+ assert shiny_entrypoint .startswith ("shiny.express.app:" )
402+ assert pyproject_entrypoint == shiny_entrypoint
403+
404+
350405# ---------------------------------------------------------------------------
351406# Title / entrypoint override
352407# ---------------------------------------------------------------------------
0 commit comments