mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-28 10:49:56 +02:00
Bumps the minor-and-patch group in /libs/cli with 4 updates: [click](https://github.com/pallets/click), [langgraph-sdk](https://github.com/langchain-ai/langgraph), [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) and [ruff](https://github.com/astral-sh/ruff). Updates `click` from 8.3.3 to 8.4.1 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/pallets/click/releases">click's releases</a>.</em></p> <blockquote> <h2>8.4.1</h2> <p>This is the Click 8.4.1 fix release, which fixes bugs but does not otherwise change behavior and should not result in breaking changes compared to the latest feature release.</p> <p>PyPI: <a href="https://pypi.org/project/click/8.4.1/">https://pypi.org/project/click/8.4.1/</a> Changes: <a href="https://click.palletsprojects.com/page/changes/#version-8-4-1">https://click.palletsprojects.com/page/changes/#version-8-4-1</a> Milestone: <a href="https://github.com/pallets/click/milestone/32?closed=1">https://github.com/pallets/click/milestone/32?closed=1</a></p> <ul> <li><code>get_parameter_source()</code> is available during eager callbacks and type conversion again. <a href="https://redirect.github.com/pallets/click/issues/3458">#3458</a> <a href="https://redirect.github.com/pallets/click/issues/3484">#3484</a></li> <li>Zsh completion scripts parse correctly on Windows. <a href="https://redirect.github.com/pallets/click/issues/3277">#3277</a> # 3466</li> <li>Shell completion of <code>Choice</code> <code>Enum</code> values produces a valid completion result. <a href="https://redirect.github.com/pallets/click/issues/3015">#3015</a></li> <li>Fix empty byte-string handling in echo. <a href="https://redirect.github.com/pallets/click/issues/3487">#3487</a></li> <li>Fix closed file error with <code>echo_via_pager</code>. <a href="https://redirect.github.com/pallets/click/issues/3449">#3449</a></li> </ul> <h2>8.4.0</h2> <p>This is the Click 8.4.0 feature release. A feature release may include new features, remove previously deprecated code, add new deprecation, or introduce potentially breaking changes.</p> <p>We encourage everyone to upgrade. You can read more about our <a href="https://palletsprojects.com/versions">Version Support Policy</a> on our website.</p> <p>PyPI: <a href="https://pypi.org/project/click/8.4.0/">https://pypi.org/project/click/8.4.0/</a> Changes: <a href="https://click.palletsprojects.com/page/changes/#version-8-4-0">https://click.palletsprojects.com/page/changes/#version-8-4-0</a> Milestone <a href="https://github.com/pallets/click/milestone/30">https://github.com/pallets/click/milestone/30</a></p> <ul> <li> <p><code>ParamType</code> typing improvements. <a href="https://redirect.github.com/pallets/click/issues/3371">#3371</a></p> <ul> <li>:class:<code>ParamType</code> is now a generic abstract base class, parameterized by its converted value type.</li> <li>:meth:<code>~ParamType.convert</code> return types are narrowed on all concrete types (<code>str</code> for :class:<code>STRING</code>, <code>int</code> for :class:<code>INT</code>, etc.).</li> <li>:meth:<code>~ParamType.to_info_dict</code> returns specific :class:<code>~typing.TypedDict</code> subclasses instead of <code>dict[str, Any]</code>.</li> <li>:class:<code>CompositeParamType</code> and the number-range base are now generic with abstract methods.</li> </ul> </li> <li> <p>Refactor <code>convert_type</code> to extract type inference into a private <code>_guess_type</code> helper, and add :func:<code>typing.overload</code> signatures. <a href="https://redirect.github.com/pallets/click/issues/3372">#3372</a></p> </li> <li> <p><code>Parameter</code> typing improvements. <a href="https://redirect.github.com/pallets/click/issues/2805">#2805</a></p> <ul> <li>:class:<code>Parameter</code> is now an abstract base class, making explicit that it cannot be instantiated directly.</li> <li>:attr:<code>Parameter.name</code> is now <code>str</code> instead of <code>str | None</code>. When <code>expose_value=False</code>, the name is set to <code>""</code> instead of <code>None</code>.</li> <li>The <code>ctx</code> parameter of :meth:<code>Parameter.get_error_hint</code> is now typed as <code>Context | None</code>, matching the runtime behavior.</li> </ul> </li> <li> <p>Split string values from <code>default_map</code> for parameters with <code>nargs > 1</code> or :class:<code>Tuple</code> type, matching environment variable behavior.</p> </li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/pallets/click/blob/main/CHANGES.rst">click's changelog</a>.</em></p> <blockquote> <h2>Version 8.4.1</h2> <p>Released 2026-05-21</p> <ul> <li><code>get_parameter_source()</code> is available during eager callbacks and type conversion again. :issue:<code>3458</code> :issue:<code>3484</code></li> <li>Zsh completion scripts parse correctly on Windows. :issue:<code>3277</code> :pr:<code>3466</code></li> <li>Shell completion of <code>Choice</code> <code>Enum</code> values produces a valid completion result. :issue:<code>3015</code></li> <li>Fix empty byte-string handling in echo. :issue:<code>3487</code></li> <li>Fix closed file error with <code>echo_via_pager</code>. :issue:<code>3449</code></li> </ul> <h2>Version 8.4.0</h2> <p>Released 2026-05-17</p> <ul> <li> <p>:class:<code>ParamType</code> typing improvements. :pr:<code>3371</code></p> <ul> <li>:class:<code>ParamType</code> is now a generic abstract base class, parameterized by its converted value type.</li> <li>:meth:<code>~ParamType.convert</code> return types are narrowed on all concrete types (<code>str</code> for :class:<code>STRING</code>, <code>int</code> for :class:<code>INT</code>, etc.).</li> <li>:meth:<code>~ParamType.to_info_dict</code> returns specific :class:<code>~typing.TypedDict</code> subclasses instead of <code>dict[str, Any]</code>.</li> <li>:class:<code>CompositeParamType</code> and the number-range base are now generic with abstract methods.</li> </ul> </li> <li> <p>Refactor <code>convert_type</code> to extract type inference into a private <code>_guess_type</code> helper, and add :func:<code>typing.overload</code> signatures. :pr:<code>3372</code></p> </li> <li> <p>:class:<code>Parameter</code> typing improvements. :pr:<code>2805</code></p> <ul> <li>:class:<code>Parameter</code> is now an abstract base class, making explicit that it cannot be instantiated directly.</li> <li>:attr:<code>Parameter.name</code> is now <code>str</code> instead of <code>str | None</code>. When <code>expose_value=False</code>, the name is set to <code>""</code> instead of <code>None</code>.</li> <li>The <code>ctx</code> parameter of :meth:<code>Parameter.get_error_hint</code> is now typed as <code>Context | None</code>, matching the runtime behavior.</li> </ul> </li> <li> <p>Split string values from <code>default_map</code> for parameters with <code>nargs > 1</code> or :class:<code>Tuple</code> type, matching environment variable behavior. :issue:<code>2745</code> :pr:<code>3364</code></p> </li> <li> <p>Auto-detect <code>type=UNPROCESSED</code> for <code>flag_value</code> of non-basic types (not <code>str</code>, <code>int</code>, <code>float</code>, or <code>bool</code>), so programmer-provided Python objects like classes and enum members are passed through unchanged instead of being stringified. Previously <code>type=click.UNPROCESSED</code> had to be set explicitly. :issue:<code>2012</code> :pr:<code>3363</code></p> </li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/pallets/click/commit/6eeb50e948ea136db145280f6f5dd52eca3fa7e5"><code>6eeb50e</code></a> release version 8.4.1</li> <li><a href="https://github.com/pallets/click/commit/67921d5b71584112eebcbf89596b5f0e6d14c49f"><code>67921d5</code></a> change log and doc fixes (<a href="https://redirect.github.com/pallets/click/issues/3495">#3495</a>)</li> <li><a href="https://github.com/pallets/click/commit/9c41f46a4015700489ad009266edf1f3893d01d1"><code>9c41f46</code></a> Fix changelog and version admonitions</li> <li><a href="https://github.com/pallets/click/commit/6cb34774f20598aa288332f8da02c5aee85448a6"><code>6cb3477</code></a> fix skip condition</li> <li><a href="https://github.com/pallets/click/commit/5ee8e3123d8ddece6c47eff9a7a7d4ca478c4f37"><code>5ee8e31</code></a> fix I/O operation on closed file error with CliRunner and echo_via_pager (<a href="https://redirect.github.com/pallets/click/issues/3482">#3482</a>)</li> <li><a href="https://github.com/pallets/click/commit/becbde5cf416441627f779e8dd34e57738ee1c1f"><code>becbde5</code></a> pager doesn't close std streams</li> <li><a href="https://github.com/pallets/click/commit/a5f5aa6d4012d256ccca24638f2642fc371e9f77"><code>a5f5aa6</code></a> Handle empty bytes in echo (<a href="https://redirect.github.com/pallets/click/issues/3493">#3493</a>)</li> <li><a href="https://github.com/pallets/click/commit/4d3db84b251518e97299a38a5ca4bab3d01873a2"><code>4d3db84</code></a> handle empty bytes in echo</li> <li><a href="https://github.com/pallets/click/commit/d42f15b71757de791a5781fb179fd972da9169f5"><code>d42f15b</code></a> Fix <code>get_parameter_source()</code> during type conversion and eager callbacks (<a href="https://redirect.github.com/pallets/click/issues/3484">#3484</a>)</li> <li><a href="https://github.com/pallets/click/commit/0baa8db07736fc7ad3d3eed97d4c73b0059c63e1"><code>0baa8db</code></a> Document ctx.params bypass with test and doc</li> <li>Additional commits viewable in <a href="https://github.com/pallets/click/compare/8.3.3...8.4.1">compare view</a></li> </ul> </details> <br /> Updates `langgraph-sdk` from 0.3.13 to 0.4.2 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/langchain-ai/langgraph/releases">langgraph-sdk's releases</a>.</em></p> <blockquote> <h2>langgraph-sdk==0.4.2</h2> <p>Changes since sdk==0.4.1</p> <ul> <li>release(sdk-py): 0.4.2 (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7955">#7955</a>)</li> <li>fix(sdk-py): percent-encode thread_id in v3 stream transport default paths (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7954">#7954</a>)</li> </ul> <h2>langgraph-sdk==0.4.1</h2> <p>Changes since sdk==0.4.0</p> <ul> <li>release(sdk-py): 0.4.1 (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7944">#7944</a>)</li> <li>feat(sdk-py): extract stream decoders and add interleave_projections (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7935">#7935</a>)</li> <li>feat(langgraph): add v3 streaming support to RemoteGraph (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7927">#7927</a>)</li> <li>fix(sdk-py): make <code>tools_agent</code> fake model stateless (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7930">#7930</a>)</li> </ul> <h2>langgraph-sdk==0.4.0</h2> <p>Changes since sdk==0.3.15</p> <ul> <li>release(sdk-py): 0.4.0 (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7923">#7923</a>)</li> <li>feat(sdk-py): add thread stream helpers (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7833">#7833</a>)</li> <li>feat(sdk-py): wire websocket stream selection (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7832">#7832</a>)</li> <li>feat(sdk-py): add websocket stream transports (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7830">#7830</a>)</li> <li>feat(sdk-py): harden streaming reconnects (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7829">#7829</a>)</li> <li>feat(sdk-py): add sync scoped subgraphs (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7828">#7828</a>)</li> <li>feat(sdk-py): add sync messages and tool calls (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7827">#7827</a>)</li> <li>feat(sdk-py): add sync thread stream core (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7826">#7826</a>)</li> <li>feat(sdk-py): add async stream reconnect support (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7825">#7825</a>)</li> <li>feat(sdk-py): add scoped subgraph handles (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7824">#7824</a>)</li> <li>feat(sdk-py): add messages and tool call projections (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7823">#7823</a>)</li> <li>feat(sdk-py): add output, values, and controller extraction (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7822">#7822</a>)</li> <li>feat(sdk-py): wire lifecycle state and output prerequisites (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7821">#7821</a>)</li> <li>feat(sdk-py): add shared stream subscriptions (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7820">#7820</a>)</li> <li>feat(sdk-py): add async thread stream skeleton (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7819">#7819</a>)</li> <li>feat(sdk-py): add v3 streaming primitives and SSE transport (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7818">#7818</a>)</li> <li>chore(langgraph): bump version to 1.2.2 (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7914">#7914</a>)</li> </ul> <h2>langgraph-sdk==0.3.15</h2> <p>Changes since sdk==0.3.14</p> <ul> <li>release(checkpoint): 4.1.1 (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7890">#7890</a>)</li> <li>release(sdk-py): 0.3.15 (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7891">#7891</a>)</li> <li>fix(sdk-py): percent-encode caller-supplied identifiers in URL paths (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7893">#7893</a>)</li> <li>release(langgraph): 1.2.1 (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7883">#7883</a>)</li> <li>chore(deps): bump idna from 3.11 to 3.15 in /libs/sdk-py (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7863">#7863</a>)</li> <li>chore(deps): bump urllib3 from 2.6.3 to 2.7.0 in /libs/sdk-py (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7764">#7764</a>)</li> <li>chore(deps): bump langsmith from 0.7.31 to 0.8.0 in /libs/sdk-py (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7789">#7789</a>)</li> <li>release: bump alpha packages to official versions (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7775">#7775</a>)</li> <li>chore(langgraph): bump langchain-core to 1.4.0 (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7767">#7767</a>)</li> <li>feat(sdk-py): support metadata filter for crons search/count (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7737">#7737</a>)</li> <li>chore(deps): bump ty from 0.0.23 to 0.0.33 in /libs/sdk-py (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/7666">#7666</a>)</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/langchain-ai/langgraph/commit/4aeaffef4e0d14de09fa4e34636372cb01992c6a"><code>4aeaffe</code></a> 0.4.2 (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/4570">#4570</a>)</li> <li><a href="https://github.com/langchain-ai/langgraph/commit/f1bfd6051a7a3e840c3a9ddc437a2e7f14dd41c8"><code>f1bfd60</code></a> update</li> <li><a href="https://github.com/langchain-ai/langgraph/commit/998be75f3430d87f5fa361c790b41ae8314fc64e"><code>998be75</code></a> langgraph: decouple name from assistant ID in RemoteGraph</li> <li><a href="https://github.com/langchain-ai/langgraph/commit/365dd5f459e2bd9f4fcf38805837fc8cf9356869"><code>365dd5f</code></a> sdk-py: Prefix private functions in sdk with _</li> <li><a href="https://github.com/langchain-ai/langgraph/commit/0e61cc2cf6100e0e053614bc2c5dcdeec04b07a8"><code>0e61cc2</code></a> prebuilt: remove state_modifier (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/4439">#4439</a>)</li> <li><a href="https://github.com/langchain-ai/langgraph/commit/a0a302dec50af869c07851210f3a9dd77200e8a0"><code>a0a302d</code></a> prebuilt: switch to executing parallel tool calls via Send by default (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/4438">#4438</a>)</li> <li><a href="https://github.com/langchain-ai/langgraph/commit/4e05db85374a652c8ddfa26b9adb9ff638fce581"><code>4e05db8</code></a> Release checkpoint-sqlite (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/4509">#4509</a>)</li> <li><a href="https://github.com/langchain-ai/langgraph/commit/d6e20e6d093a4fe53cb04afa5af8a5b3d3b8ef31"><code>d6e20e6</code></a> Add missing 'running' RunStatus (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/4508">#4508</a>)</li> <li><a href="https://github.com/langchain-ai/langgraph/commit/fcd06acd3365aa2c7ba5091349a6f850aedd09c4"><code>fcd06ac</code></a> Add support for specifying a custom base image in docker commands (<a href="https://redirect.github.com/langchain-ai/langgraph/issues/4500">#4500</a>)</li> <li><a href="https://github.com/langchain-ai/langgraph/commit/94994a8d995596c1a3c035572062987c71811d83"><code>94994a8</code></a> meaningless commit to get vercel going sigh</li> <li>Additional commits viewable in <a href="https://github.com/langchain-ai/langgraph/compare/0.3.13...0.4.2">compare view</a></li> </ul> </details> <br /> Updates `pytest-asyncio` from 1.3.0 to 1.4.0 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/pytest-dev/pytest-asyncio/releases">pytest-asyncio's releases</a>.</em></p> <blockquote> <h2>pytest-asyncio v1.4.0</h2> <h1><a href="https://github.com/pytest-dev/pytest-asyncio/tree/1.4.0">1.4.0</a> - 2026-05-26</h1> <h2>Deprecated</h2> <ul> <li>Overriding the <em>event_loop_policy</em> fixture is deprecated. Use the <code>pytest_asyncio_loop_factories</code> hook instead. (<a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/1419">#1419</a>)</li> </ul> <h2>Added</h2> <ul> <li> <p>Added the <code>pytest_asyncio_loop_factories</code> hook to parametrize asyncio tests with custom event loop factories.</p> <p>The hook returns a mapping of factory names to loop factories, and <code>pytest.mark.asyncio(loop_factories=[...])</code> selects a subset of configured factories per test. When a single factory is configured, test names are unchanged.</p> <p>Synchronous <code>@pytest_asyncio.fixture</code> functions now see the correct event loop when custom loop factories are configured, even when test code disrupts the current event loop (e.g., via <code>asyncio.run()</code> or <code>asyncio.set_event_loop(None)</code>). (<a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/1164">#1164</a>)</p> </li> </ul> <h2>Changed</h2> <ul> <li>Improved the readability of the warning message that is displayed when <code>asyncio_default_fixture_loop_scope</code> is unset (<a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/1298">#1298</a>)</li> <li>Only import <code>asyncio.AbstractEventLoopPolicy</code> for type checking to avoid raising a DeprecationWarning. (<a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/1394">#1394</a>)</li> <li>Updated minimum supported pytest version to v8.4.0. (<a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/1397">#1397</a>)</li> </ul> <h2>Fixed</h2> <ul> <li>Fixed a <code>ResourceWarning: unclosed event loop</code> warning that could occur when a synchronous test called <code>asyncio.run()</code> or otherwise unset the current event loop after pytest-asyncio had run an async test or fixture. (<a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/724">#724</a>)</li> </ul> <h2>Notes for Downstream Packagers</h2> <ul> <li>Added dependency on <code>sphinx-tabs >= 3.5</code> to organize documentation examples into tabs. (<a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/1395">#1395</a>)</li> </ul> <h2>pytest-asyncio v1.4.0a2</h2> <h1><a href="https://github.com/pytest-dev/pytest-asyncio/tree/1.4.0a2">1.4.0a2</a> - 2026-05-02</h1> <h2>Deprecated</h2> <ul> <li>Overriding the <em>event_loop_policy</em> fixture is deprecated. Use the <code>pytest_asyncio_loop_factories</code> hook instead. (<a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/1419">#1419</a>)</li> </ul> <h2>Added</h2> <ul> <li> <p>Added the <code>pytest_asyncio_loop_factories</code> hook to parametrize asyncio tests with custom event loop factories.</p> <p>The hook returns a mapping of factory names to loop factories, and <code>pytest.mark.asyncio(loop_factories=[...])</code> selects a subset of configured factories per test. When a single factory is configured, test names are unchanged on pytest 8.4+.</p> <p>Synchronous <code>@pytest_asyncio.fixture</code> functions now see the correct event loop when custom loop factories are configured, even when test code disrupts the current event loop (e.g., via <code>asyncio.run()</code> or <code>asyncio.set_event_loop(None)</code>). (<a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/1164">#1164</a>)</p> </li> </ul> <h2>Changed</h2> <ul> <li>Improved the readability of the warning message that is displayed when <code>asyncio_default_fixture_loop_scope</code> is unset (<a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/1298">#1298</a>)</li> <li>Only import <code>asyncio.AbstractEventLoopPolicy</code> for type checking to avoid raising a DeprecationWarning. (<a href="https://redirect.github.com/pytest-dev/pytest-asyncio/issues/1394">#1394</a>)</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/6e14cd2af9292dca1fa2b027a06bbc40b0e0e425"><code>6e14cd2</code></a> chore: Prepare release of v1.4.0.</li> <li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/4b900fb5d0c30949c574e55dd904ee179f858a5e"><code>4b900fb</code></a> Build(deps): Bump codecov/codecov-action from 6.0.0 to 6.0.1</li> <li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/ab9f63245094865c42c940a34af724b0dec1debf"><code>ab9f632</code></a> Build(deps): Bump zipp from 3.23.1 to 4.1.0</li> <li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/a56fc77ecd59f781d8471b0f6a82bf58e08c95fa"><code>a56fc77</code></a> Build(deps): Bump hypothesis from 6.152.6 to 6.152.8</li> <li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/e8bae9bc1f197731fc1a210c0da557af7b698e6d"><code>e8bae9b</code></a> Build(deps): Bump requests from 2.34.0 to 2.34.2</li> <li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/fc433402c570fd36a7a227ef4bc3abd4579299de"><code>fc43340</code></a> Build(deps): Bump idna from 3.14 to 3.15</li> <li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/762eaf5033b798b965c92afdbb2cebefa8fc3a8b"><code>762eaf5</code></a> Build(deps): Bump jaraco-functools from 4.4.0 to 4.5.0</li> <li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/b62e2228c80070977baf6b77ba89d5c148af920f"><code>b62e222</code></a> Build(deps): Bump click from 8.3.3 to 8.4.0</li> <li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/919044700627889d25ca63b6e7a3bc785f3137eb"><code>9190447</code></a> Build(deps): Bump pydantic from 2.13.3 to 2.13.4</li> <li><a href="https://github.com/pytest-dev/pytest-asyncio/commit/82a393c5e31b6ebbbd8ec2a8dafc5f35b9cf1236"><code>82a393c</code></a> ci: Remove unnecessary debug output.</li> <li>Additional commits viewable in <a href="https://github.com/pytest-dev/pytest-asyncio/compare/v1.3.0...v1.4.0">compare view</a></li> </ul> </details> <br /> Updates `ruff` from 0.15.12 to 0.15.15 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ruff/releases">ruff's releases</a>.</em></p> <blockquote> <h2>0.15.15</h2> <h2>Release Notes</h2> <p>Released on 2026-05-28.</p> <h3>Preview features</h3> <ul> <li>Fix Markdown closing fence handling (<a href="https://redirect.github.com/astral-sh/ruff/pull/25310">#25310</a>)</li> <li>[<code>pyflakes</code>] Report duplicate imports in <code>typing.TYPE_CHECKING</code> block (<code>F811</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/22560">#22560</a>)</li> </ul> <h3>Bug fixes</h3> <ul> <li>[<code>pyflakes</code>] Treat function-scope bare annotations as locals per PEP 526 (<code>F821</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/21540">#21540</a>)</li> </ul> <h3>Performance</h3> <ul> <li>Avoid redundant <code>TokenValue</code> drops in the lexer (<a href="https://redirect.github.com/astral-sh/ruff/pull/25300">#25300</a>)</li> <li>Reduce memory usage by dropping token-excess capacity and improve performance by approximating the initial tokens <code>Vec</code> size (<a href="https://redirect.github.com/astral-sh/ruff/pull/25354">#25354</a>)</li> <li>Use <code>ThinVec</code> in AST to shrink <code>Stmt</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/25361">#25361</a>)</li> </ul> <h3>Documentation</h3> <ul> <li>Fix <code>line-length</code> example for <code>--config</code> option (<a href="https://redirect.github.com/astral-sh/ruff/pull/25389">#25389</a>)</li> <li>[<code>flake8-comprehensions</code>] Document <code>RecursionError</code> edge case in <code>__len__</code> (<code>C416</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/25286">#25286</a>)</li> <li>[<code>mccabe</code>] Improve example (<code>C901</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/25287">#25287</a>)</li> <li>[<code>pyupgrade</code>] Clarify fix safety docs (<code>UP007</code>, <code>UP045</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/25288">#25288</a>)</li> <li>[<code>refurb</code>] Document <code>FURB192</code> exception change for empty sequences (<a href="https://redirect.github.com/astral-sh/ruff/pull/25317">#25317</a>)</li> <li>[<code>ruff</code>] Document false negative for user-defined types (<code>RUF013</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/25289">#25289</a>)</li> </ul> <h3>Formatter</h3> <ul> <li>Fix formatting of lambdas nested within f-strings (<a href="https://redirect.github.com/astral-sh/ruff/pull/25398">#25398</a>)</li> </ul> <h3>Server</h3> <ul> <li>Return code action for <code>codeAction/resolve</code> requests that contain no or no valid URL (<a href="https://redirect.github.com/astral-sh/ruff/pull/25365">#25365</a>)</li> </ul> <h3>Other changes</h3> <ul> <li>Expand semantic syntax errors for invalid walruses (<a href="https://redirect.github.com/astral-sh/ruff/pull/25415">#25415</a>)</li> </ul> <h3>Contributors</h3> <ul> <li><a href="https://github.com/chirizxc"><code>@chirizxc</code></a></li> <li><a href="https://github.com/ntBre"><code>@ntBre</code></a></li> <li><a href="https://github.com/adityasingh2400"><code>@adityasingh2400</code></a></li> <li><a href="https://github.com/charliermarsh"><code>@charliermarsh</code></a></li> <li><a href="https://github.com/fallintoplace"><code>@fallintoplace</code></a></li> <li><a href="https://github.com/martin-schlossarek"><code>@martin-schlossarek</code></a></li> <li><a href="https://github.com/MichaReiser"><code>@MichaReiser</code></a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md">ruff's changelog</a>.</em></p> <blockquote> <h2>0.15.15</h2> <p>Released on 2026-05-28.</p> <h3>Preview features</h3> <ul> <li>Fix Markdown closing fence handling (<a href="https://redirect.github.com/astral-sh/ruff/pull/25310">#25310</a>)</li> <li>[<code>pyflakes</code>] Report duplicate imports in <code>typing.TYPE_CHECKING</code> block (<code>F811</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/22560">#22560</a>)</li> </ul> <h3>Bug fixes</h3> <ul> <li>[<code>pyflakes</code>] Treat function-scope bare annotations as locals per PEP 526 (<code>F821</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/21540">#21540</a>)</li> </ul> <h3>Performance</h3> <ul> <li>Avoid redundant <code>TokenValue</code> drops in the lexer (<a href="https://redirect.github.com/astral-sh/ruff/pull/25300">#25300</a>)</li> <li>Reduce memory usage by dropping token-excess capacity and improve performance by approximating the initial tokens <code>Vec</code> size (<a href="https://redirect.github.com/astral-sh/ruff/pull/25354">#25354</a>)</li> <li>Use <code>ThinVec</code> in AST to shrink <code>Stmt</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/25361">#25361</a>)</li> </ul> <h3>Documentation</h3> <ul> <li>Fix <code>line-length</code> example for <code>--config</code> option (<a href="https://redirect.github.com/astral-sh/ruff/pull/25389">#25389</a>)</li> <li>[<code>flake8-comprehensions</code>] Document <code>RecursionError</code> edge case in <code>__len__</code> (<code>C416</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/25286">#25286</a>)</li> <li>[<code>mccabe</code>] Improve example (<code>C901</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/25287">#25287</a>)</li> <li>[<code>pyupgrade</code>] Clarify fix safety docs (<code>UP007</code>, <code>UP045</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/25288">#25288</a>)</li> <li>[<code>refurb</code>] Document <code>FURB192</code> exception change for empty sequences (<a href="https://redirect.github.com/astral-sh/ruff/pull/25317">#25317</a>)</li> <li>[<code>ruff</code>] Document false negative for user-defined types (<code>RUF013</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/25289">#25289</a>)</li> </ul> <h3>Formatter</h3> <ul> <li>Fix formatting of lambdas nested within f-strings (<a href="https://redirect.github.com/astral-sh/ruff/pull/25398">#25398</a>)</li> </ul> <h3>Server</h3> <ul> <li>Return code action for <code>codeAction/resolve</code> requests that contain no or no valid URL (<a href="https://redirect.github.com/astral-sh/ruff/pull/25365">#25365</a>)</li> </ul> <h3>Other changes</h3> <ul> <li>Expand semantic syntax errors for invalid walruses (<a href="https://redirect.github.com/astral-sh/ruff/pull/25415">#25415</a>)</li> </ul> <h3>Contributors</h3> <ul> <li><a href="https://github.com/chirizxc"><code>@chirizxc</code></a></li> <li><a href="https://github.com/ntBre"><code>@ntBre</code></a></li> <li><a href="https://github.com/adityasingh2400"><code>@adityasingh2400</code></a></li> <li><a href="https://github.com/charliermarsh"><code>@charliermarsh</code></a></li> <li><a href="https://github.com/fallintoplace"><code>@fallintoplace</code></a></li> <li><a href="https://github.com/martin-schlossarek"><code>@martin-schlossarek</code></a></li> <li><a href="https://github.com/MichaReiser"><code>@MichaReiser</code></a></li> <li><a href="https://github.com/Ruchir28"><code>@Ruchir28</code></a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/astral-sh/ruff/commit/db5aa0a5f1b92cb91d910bf0866a967554dd94f5"><code>db5aa0a</code></a> Bump 0.15.15 (<a href="https://redirect.github.com/astral-sh/ruff/issues/25431">#25431</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/366fe21ba369ccdd01eb99c1043c9a969c99230b"><code>366fe21</code></a> [ty] Improve diagnostics for syntax errors in forward annotations (<a href="https://redirect.github.com/astral-sh/ruff/issues/25158">#25158</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/e2e1e647d182b8567845039c9a65fb0608a4dcfc"><code>e2e1e64</code></a> [ty] Remove excess capacity from more Salsa cached collections (<a href="https://redirect.github.com/astral-sh/ruff/issues/25411">#25411</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/1bd77e1646f2213d86b8da215f08279187867d72"><code>1bd77e1</code></a> [ty] Use diagnostic message as tie breaker when sorting (<a href="https://redirect.github.com/astral-sh/ruff/issues/25424">#25424</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/7e1bc1e75f15795f12c846294b13df4535f2abbf"><code>7e1bc1e</code></a> Add agent skills for working on ty (<a href="https://redirect.github.com/astral-sh/ruff/issues/25422">#25422</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/574e10752f8cfa9e0cdbe3b01e96c4380950469b"><code>574e107</code></a> Expand semantic syntax errors for invalid walruses (<a href="https://redirect.github.com/astral-sh/ruff/issues/25415">#25415</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/4a7ca062fccd80443a43aa61e5dc7e5858e88dc1"><code>4a7ca06</code></a> [ty] Display docs for matching parameter when hovering over the name of an ar...</li> <li><a href="https://github.com/astral-sh/ruff/commit/54327092dbfe455040690d63bb1e5e4b5f551239"><code>5432709</code></a> Refine a few agents instructions (<a href="https://redirect.github.com/astral-sh/ruff/issues/25423">#25423</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/3cb09eba689ebb49e799131092121928cc789c18"><code>3cb09eb</code></a> [ty] Support <code>typing.TypeForm</code> (<a href="https://redirect.github.com/astral-sh/ruff/issues/25334">#25334</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/c8cd59f189f2b6f55d542b29bddb953622add6fc"><code>c8cd59f</code></a> [ty] Infer class attributes assigned by metaclass initialization (<a href="https://redirect.github.com/astral-sh/ruff/issues/25342">#25342</a>)</li> <li>Additional commits viewable in <a href="https://github.com/astral-sh/ruff/compare/0.15.12...0.15.15">compare view</a></li> </ul> </details> <br /> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
1332 lines
42 KiB
Python
1332 lines
42 KiB
Python
import json
|
||
import pathlib
|
||
import re
|
||
import shutil
|
||
import tempfile
|
||
import textwrap
|
||
from contextlib import contextmanager
|
||
from pathlib import Path
|
||
|
||
import click
|
||
from click.testing import CliRunner
|
||
|
||
import langgraph_cli.deploy as deploy_module
|
||
from langgraph_cli.cli import cli, prepare_args_and_stdin
|
||
from langgraph_cli.config import Config, _get_pip_cleanup_lines, validate_config
|
||
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
|
||
from langgraph_cli.util import clean_empty_lines
|
||
|
||
FORMATTED_CLEANUP_LINES = _get_pip_cleanup_lines(
|
||
install_cmd="uv pip install --system",
|
||
to_uninstall=("pip", "setuptools", "wheel"),
|
||
pip_installer="uv",
|
||
)
|
||
DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
|
||
version_docker=Version(26, 1, 1),
|
||
version_compose=Version(2, 27, 0),
|
||
healthcheck_start_interval=True,
|
||
)
|
||
|
||
|
||
@contextmanager
|
||
def temporary_config_folder(config_content: dict, levels: int = 0):
|
||
# Create a temporary directory
|
||
temp_dir = tempfile.mkdtemp()
|
||
try:
|
||
# Define the path for the config.json file
|
||
config_path = Path(temp_dir) / f"{'a/' * levels}config.json"
|
||
# Ensure the parent directory exists
|
||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
# Write the provided dictionary content to config.json
|
||
with open(config_path, "w", encoding="utf-8") as config_file:
|
||
json.dump(config_content, config_file)
|
||
|
||
# Yield the temporary directory path for use within the context
|
||
yield config_path.parent
|
||
finally:
|
||
# Cleanup the temporary directory and its contents
|
||
shutil.rmtree(temp_dir)
|
||
|
||
|
||
def test_prepare_args_and_stdin() -> None:
|
||
# this basically serves as an end-to-end test for using config and docker helpers
|
||
config_path = pathlib.Path(__file__).parent / "langgraph.json"
|
||
config = validate_config(
|
||
Config(dependencies=[".", "../../.."], graphs={"agent": "agent.py:graph"})
|
||
)
|
||
port = 8000
|
||
debugger_port = 8001
|
||
debugger_graph_url = f"http://127.0.0.1:{port}"
|
||
|
||
actual_args, actual_stdin = prepare_args_and_stdin(
|
||
capabilities=DEFAULT_DOCKER_CAPABILITIES,
|
||
config_path=config_path,
|
||
config=config,
|
||
docker_compose=pathlib.Path("custom-docker-compose.yml"),
|
||
port=port,
|
||
debugger_port=debugger_port,
|
||
debugger_base_url=debugger_graph_url,
|
||
watch=True,
|
||
)
|
||
|
||
expected_args = [
|
||
"--project-directory",
|
||
str(pathlib.Path(__file__).parent.absolute()),
|
||
"-f",
|
||
"custom-docker-compose.yml",
|
||
"-f",
|
||
"-",
|
||
]
|
||
expected_stdin = f"""volumes:
|
||
langgraph-data:
|
||
driver: local
|
||
services:
|
||
langgraph-redis:
|
||
image: redis:6
|
||
healthcheck:
|
||
test: redis-cli ping
|
||
interval: 5s
|
||
timeout: 1s
|
||
retries: 5
|
||
langgraph-postgres:
|
||
image: pgvector/pgvector:pg16
|
||
ports:
|
||
- "5433:5432"
|
||
environment:
|
||
POSTGRES_DB: postgres
|
||
POSTGRES_USER: postgres
|
||
POSTGRES_PASSWORD: postgres
|
||
command:
|
||
- postgres
|
||
- -c
|
||
- shared_preload_libraries=vector
|
||
volumes:
|
||
- langgraph-data:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: pg_isready -U postgres
|
||
start_period: 10s
|
||
timeout: 1s
|
||
retries: 5
|
||
interval: 60s
|
||
start_interval: 1s
|
||
langgraph-debugger:
|
||
image: langchain/langgraph-debugger
|
||
restart: on-failure
|
||
depends_on:
|
||
langgraph-postgres:
|
||
condition: service_healthy
|
||
ports:
|
||
- "{debugger_port}:3968"
|
||
environment:
|
||
VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
|
||
langgraph-api:
|
||
ports:
|
||
- "8000:8000"
|
||
depends_on:
|
||
langgraph-redis:
|
||
condition: service_healthy
|
||
langgraph-postgres:
|
||
condition: service_healthy
|
||
environment:
|
||
REDIS_URI: redis://langgraph-redis:6379
|
||
POSTGRES_URI: {DEFAULT_POSTGRES_URI}
|
||
healthcheck:
|
||
test: python /api/healthcheck.py
|
||
interval: 60s
|
||
start_interval: 1s
|
||
start_period: 10s
|
||
|
||
pull_policy: build
|
||
build:
|
||
context: .
|
||
additional_contexts:
|
||
- cli_1: {str(pathlib.Path(__file__).parent.parent.parent.parent.absolute())}
|
||
dockerfile_inline: |
|
||
# syntax=docker/dockerfile:1.4
|
||
FROM langchain/langgraph-api:3.11
|
||
# -- Adding local package . --
|
||
ADD . /deps/cli
|
||
# -- End of local package . --
|
||
# -- Adding local package ../../.. --
|
||
COPY --from=cli_1 . /deps/cli_1
|
||
# -- End of local package ../../.. --
|
||
# -- Installing all local dependencies --
|
||
RUN for dep in /deps/*; do echo "Installing $$dep"; if [ -d "$$dep" ]; then echo "Installing $$dep"; (cd "$$dep" && PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e .); fi; done
|
||
# -- End of local dependencies install --
|
||
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
||
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||
WORKDIR /deps/cli
|
||
|
||
develop:
|
||
watch:
|
||
- path: langgraph.json
|
||
action: rebuild
|
||
- path: .
|
||
action: rebuild
|
||
- path: ../../..
|
||
action: rebuild\
|
||
"""
|
||
assert actual_args == expected_args
|
||
assert clean_empty_lines(actual_stdin) == expected_stdin
|
||
|
||
|
||
def test_prepare_args_and_stdin_with_image() -> None:
|
||
# this basically serves as an end-to-end test for using config and docker helpers
|
||
config_path = pathlib.Path(__file__).parent / "langgraph.json"
|
||
config = validate_config(
|
||
Config(dependencies=[".", "../../.."], graphs={"agent": "agent.py:graph"})
|
||
)
|
||
port = 8000
|
||
debugger_port = 8001
|
||
debugger_graph_url = f"http://127.0.0.1:{port}"
|
||
|
||
actual_args, actual_stdin = prepare_args_and_stdin(
|
||
capabilities=DEFAULT_DOCKER_CAPABILITIES,
|
||
config_path=config_path,
|
||
config=config,
|
||
docker_compose=pathlib.Path("custom-docker-compose.yml"),
|
||
port=port,
|
||
debugger_port=debugger_port,
|
||
debugger_base_url=debugger_graph_url,
|
||
watch=True,
|
||
image="my-cool-image",
|
||
)
|
||
|
||
expected_args = [
|
||
"--project-directory",
|
||
str(pathlib.Path(__file__).parent.absolute()),
|
||
"-f",
|
||
"custom-docker-compose.yml",
|
||
"-f",
|
||
"-",
|
||
]
|
||
expected_stdin = f"""volumes:
|
||
langgraph-data:
|
||
driver: local
|
||
services:
|
||
langgraph-redis:
|
||
image: redis:6
|
||
healthcheck:
|
||
test: redis-cli ping
|
||
interval: 5s
|
||
timeout: 1s
|
||
retries: 5
|
||
langgraph-postgres:
|
||
image: pgvector/pgvector:pg16
|
||
ports:
|
||
- "5433:5432"
|
||
environment:
|
||
POSTGRES_DB: postgres
|
||
POSTGRES_USER: postgres
|
||
POSTGRES_PASSWORD: postgres
|
||
command:
|
||
- postgres
|
||
- -c
|
||
- shared_preload_libraries=vector
|
||
volumes:
|
||
- langgraph-data:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: pg_isready -U postgres
|
||
start_period: 10s
|
||
timeout: 1s
|
||
retries: 5
|
||
interval: 60s
|
||
start_interval: 1s
|
||
langgraph-debugger:
|
||
image: langchain/langgraph-debugger
|
||
restart: on-failure
|
||
depends_on:
|
||
langgraph-postgres:
|
||
condition: service_healthy
|
||
ports:
|
||
- "{debugger_port}:3968"
|
||
environment:
|
||
VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
|
||
langgraph-api:
|
||
ports:
|
||
- "8000:8000"
|
||
depends_on:
|
||
langgraph-redis:
|
||
condition: service_healthy
|
||
langgraph-postgres:
|
||
condition: service_healthy
|
||
environment:
|
||
REDIS_URI: redis://langgraph-redis:6379
|
||
POSTGRES_URI: {DEFAULT_POSTGRES_URI}
|
||
image: my-cool-image
|
||
healthcheck:
|
||
test: python /api/healthcheck.py
|
||
interval: 60s
|
||
start_interval: 1s
|
||
start_period: 10s
|
||
|
||
|
||
develop:
|
||
watch:
|
||
- path: langgraph.json
|
||
action: rebuild
|
||
- path: .
|
||
action: rebuild
|
||
- path: ../../..
|
||
action: rebuild\
|
||
"""
|
||
assert actual_args == expected_args
|
||
assert clean_empty_lines(actual_stdin) == expected_stdin
|
||
|
||
|
||
def test_version_option() -> None:
|
||
"""Test the --version option of the CLI."""
|
||
runner = CliRunner()
|
||
result = runner.invoke(cli, ["--version"])
|
||
|
||
# Verify that the command executed successfully
|
||
assert result.exit_code == 0, "Expected exit code 0 for --version option"
|
||
|
||
# Check that the output contains the correct version information
|
||
assert "LangGraph CLI, version" in result.output, (
|
||
"Expected version information in output"
|
||
)
|
||
|
||
|
||
def test_top_level_help_shows_deploy_subcommands() -> None:
|
||
runner = CliRunner()
|
||
|
||
result = runner.invoke(cli, ["--help"])
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert "deploy" in result.output
|
||
assert "deploy list" in result.output
|
||
assert "deploy delete" in result.output
|
||
assert "deploy revisions" in result.output
|
||
assert "deploy revisions list" in result.output
|
||
assert "[Beta] List LangSmith Deployments." in result.output
|
||
|
||
|
||
def test_top_level_help_truncates_command_descriptions_to_single_line() -> None:
|
||
runner = CliRunner()
|
||
|
||
result = runner.invoke(cli, ["--help"])
|
||
|
||
assert result.exit_code == 0, result.output
|
||
lines = result.output.splitlines()
|
||
deploy_line = next(line for line in lines if line.strip().startswith("deploy"))
|
||
deploy_list_line = next(
|
||
line for line in lines if line.strip().startswith("deploy list")
|
||
)
|
||
|
||
assert not lines[lines.index(deploy_line) + 1].startswith(" ")
|
||
assert "..." in deploy_line
|
||
assert "[Beta] List LangSmith Deployments." in deploy_list_line
|
||
|
||
|
||
def test_deploy_list_command(monkeypatch) -> None:
|
||
runner = CliRunner()
|
||
captured: dict[str, str] = {}
|
||
|
||
class FakeClient:
|
||
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
|
||
captured["host_url"] = host_url
|
||
captured["api_key"] = api_key
|
||
captured["tenant_id"] = tenant_id or ""
|
||
|
||
def list_deployments(self, name_contains: str = ""):
|
||
captured["name_contains"] = name_contains
|
||
return {
|
||
"resources": [
|
||
{
|
||
"id": "dep-123",
|
||
"name": "alpha",
|
||
"source_config": {"custom_url": "https://alpha.example.com"},
|
||
},
|
||
{
|
||
"id": "dep-456",
|
||
"name": "beta",
|
||
"source_config": {"custom_url": "https://beta.example.com"},
|
||
},
|
||
]
|
||
}
|
||
|
||
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"deploy",
|
||
"list",
|
||
"--api-key",
|
||
"test-key",
|
||
"--host-url",
|
||
"https://api.example.com",
|
||
"--name-contains",
|
||
"alp",
|
||
],
|
||
)
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert captured == {
|
||
"host_url": "https://api.example.com",
|
||
"api_key": "test-key",
|
||
"tenant_id": "",
|
||
"name_contains": "alp",
|
||
}
|
||
assert "Deployment ID" in result.output
|
||
assert "Deployment Name" in result.output
|
||
assert "Deployment URL" in result.output
|
||
assert "dep-123" in result.output
|
||
assert "https://beta.example.com" in result.output
|
||
|
||
|
||
def test_deploy_list_command_no_results(monkeypatch) -> None:
|
||
runner = CliRunner()
|
||
|
||
class FakeClient:
|
||
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
|
||
pass
|
||
|
||
def list_deployments(self, name_contains: str = ""):
|
||
return {"resources": []}
|
||
|
||
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"deploy",
|
||
"list",
|
||
"--api-key",
|
||
"test-key",
|
||
"--host-url",
|
||
"https://api.example.com",
|
||
],
|
||
)
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert result.output.strip() == "No deployments found."
|
||
|
||
|
||
def test_deploy_revisions_list_command(monkeypatch) -> None:
|
||
runner = CliRunner()
|
||
captured: dict[str, str] = {}
|
||
|
||
class FakeClient:
|
||
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
|
||
captured["host_url"] = host_url
|
||
captured["api_key"] = api_key
|
||
captured["tenant_id"] = tenant_id or ""
|
||
|
||
def list_revisions(self, deployment_id: str, limit: int = 1):
|
||
captured["deployment_id"] = deployment_id
|
||
captured["limit"] = str(limit)
|
||
return {
|
||
"resources": [
|
||
{
|
||
"id": "rev-123",
|
||
"status": "CREATING",
|
||
"created_at": "2023-11-07T05:31:56Z",
|
||
},
|
||
{
|
||
"id": "rev-456",
|
||
"status": "DEPLOYED",
|
||
"created_at": "2023-11-08T10:00:00Z",
|
||
},
|
||
]
|
||
}
|
||
|
||
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"deploy",
|
||
"revisions",
|
||
"list",
|
||
"--api-key",
|
||
"test-key",
|
||
"--host-url",
|
||
"https://api.example.com",
|
||
"dep-123",
|
||
],
|
||
)
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert captured == {
|
||
"host_url": "https://api.example.com",
|
||
"api_key": "test-key",
|
||
"tenant_id": "",
|
||
"deployment_id": "dep-123",
|
||
"limit": "10",
|
||
}
|
||
assert "Revision ID" in result.output
|
||
assert "Status" in result.output
|
||
assert "Created At" in result.output
|
||
assert "rev-123" in result.output
|
||
assert "2023-11-08T10:00:00Z" in result.output
|
||
|
||
|
||
def test_deploy_revisions_list_command_no_results(monkeypatch) -> None:
|
||
runner = CliRunner()
|
||
|
||
class FakeClient:
|
||
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
|
||
pass
|
||
|
||
def list_revisions(self, deployment_id: str, limit: int = 1):
|
||
return {"resources": []}
|
||
|
||
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"deploy",
|
||
"revisions",
|
||
"list",
|
||
"--api-key",
|
||
"test-key",
|
||
"--host-url",
|
||
"https://api.example.com",
|
||
"dep-123",
|
||
],
|
||
)
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert result.output.strip() == "No revisions found for deployment dep-123."
|
||
|
||
|
||
def test_deploy_revisions_list_command_with_explicit_limit(monkeypatch) -> None:
|
||
runner = CliRunner()
|
||
captured: dict[str, str] = {}
|
||
|
||
class FakeClient:
|
||
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
|
||
pass
|
||
|
||
def list_revisions(self, deployment_id: str, limit: int = 1):
|
||
captured["deployment_id"] = deployment_id
|
||
captured["limit"] = str(limit)
|
||
return {"resources": []}
|
||
|
||
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"deploy",
|
||
"revisions",
|
||
"list",
|
||
"--api-key",
|
||
"test-key",
|
||
"--host-url",
|
||
"https://api.example.com",
|
||
"--limit",
|
||
"25",
|
||
"dep-123",
|
||
],
|
||
)
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert captured == {"deployment_id": "dep-123", "limit": "25"}
|
||
assert result.output.strip() == "No revisions found for deployment dep-123."
|
||
|
||
|
||
def test_deploy_revisions_list_missing_deployment_id_shows_usage() -> None:
|
||
runner = CliRunner()
|
||
|
||
result = runner.invoke(cli, ["deploy", "revisions", "list"])
|
||
|
||
assert result.exit_code == 2, result.output
|
||
assert "Missing argument 'DEPLOYMENT_ID'" in result.output
|
||
|
||
|
||
def test_deploy_delete_command(monkeypatch) -> None:
|
||
runner = CliRunner()
|
||
captured: dict[str, str] = {}
|
||
|
||
class FakeClient:
|
||
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
|
||
captured["host_url"] = host_url
|
||
captured["api_key"] = api_key
|
||
captured["tenant_id"] = tenant_id or ""
|
||
|
||
def delete_deployment(self, deployment_id: str):
|
||
captured["deployment_id"] = deployment_id
|
||
return None
|
||
|
||
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"deploy",
|
||
"delete",
|
||
"--api-key",
|
||
"test-key",
|
||
"--host-url",
|
||
"https://api.example.com",
|
||
"dep-123",
|
||
],
|
||
input="y\n",
|
||
)
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert captured == {
|
||
"host_url": "https://api.example.com",
|
||
"api_key": "test-key",
|
||
"tenant_id": "",
|
||
"deployment_id": "dep-123",
|
||
}
|
||
assert (
|
||
"Are you sure you want to delete deployment ID dep-123? (Y/n):"
|
||
in click.unstyle(result.output)
|
||
)
|
||
assert result.output.strip().endswith("Deleted deployment dep-123.")
|
||
|
||
|
||
def test_deploy_delete_command_cancelled(monkeypatch) -> None:
|
||
runner = CliRunner()
|
||
deleted = False
|
||
|
||
class FakeClient:
|
||
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
|
||
pass
|
||
|
||
def delete_deployment(self, deployment_id: str):
|
||
nonlocal deleted
|
||
deleted = True
|
||
return None
|
||
|
||
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"deploy",
|
||
"delete",
|
||
"--api-key",
|
||
"test-key",
|
||
"--host-url",
|
||
"https://api.example.com",
|
||
"dep-123",
|
||
],
|
||
input="n\n",
|
||
)
|
||
|
||
assert result.exit_code == 1, result.output
|
||
assert not deleted
|
||
assert "Aborted!" in result.output
|
||
|
||
|
||
def test_deploy_delete_command_force(monkeypatch) -> None:
|
||
runner = CliRunner()
|
||
captured: dict[str, str] = {}
|
||
|
||
class FakeClient:
|
||
def __init__(self, host_url: str, api_key: str, tenant_id: str | None = None):
|
||
captured["host_url"] = host_url
|
||
captured["api_key"] = api_key
|
||
captured["tenant_id"] = tenant_id or ""
|
||
|
||
def delete_deployment(self, deployment_id: str):
|
||
captured["deployment_id"] = deployment_id
|
||
return None
|
||
|
||
monkeypatch.setattr(deploy_module, "HostBackendClient", FakeClient)
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"deploy",
|
||
"delete",
|
||
"--force",
|
||
"--api-key",
|
||
"test-key",
|
||
"--host-url",
|
||
"https://api.example.com",
|
||
"dep-123",
|
||
],
|
||
)
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert "Are you sure you want to delete deployment ID dep-123?" not in result.output
|
||
assert captured == {
|
||
"host_url": "https://api.example.com",
|
||
"api_key": "test-key",
|
||
"tenant_id": "",
|
||
"deployment_id": "dep-123",
|
||
}
|
||
assert result.output.strip() == "Deleted deployment dep-123."
|
||
|
||
|
||
def test_dockerfile_command_basic() -> None:
|
||
"""Test the 'dockerfile' command with basic configuration."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.11",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
|
||
)
|
||
|
||
# Assert command was successful
|
||
assert result.exit_code == 0, result.output
|
||
assert "✅ Created: Dockerfile" in result.output
|
||
|
||
# Check if Dockerfile was created
|
||
assert save_path.exists()
|
||
|
||
|
||
def test_dockerfile_command_new_style_config() -> None:
|
||
"""Test `dockerfile` command with a new style config.
|
||
|
||
This config format allows specifying agent data as a dictionary.
|
||
{
|
||
"graphs": {
|
||
"agent1": {
|
||
"path": ... # path to graph definition,
|
||
... # other fields
|
||
}
|
||
}
|
||
}
|
||
"""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"dependencies": ["./my_agent"],
|
||
"graphs": {
|
||
"agent": {
|
||
"path": "./my_agent/agent.py:graph",
|
||
"description": "This is a test agent",
|
||
}
|
||
},
|
||
"env": ".env",
|
||
}
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
# Add agent.py file
|
||
agent_path = temp_dir / "my_agent" / "agent.py"
|
||
agent_path.parent.mkdir(parents=True, exist_ok=True)
|
||
agent_path.touch()
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
|
||
)
|
||
|
||
# Assert command was successful
|
||
assert result.exit_code == 0, result.output
|
||
assert "✅ Created: Dockerfile" in result.output
|
||
|
||
# Check if Dockerfile was created
|
||
assert save_path.exists()
|
||
|
||
|
||
def test_dockerfile_command_with_base_image() -> None:
|
||
"""Test the 'dockerfile' command with a base image."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.11",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
"base_image": "langchain/langgraph-server:0.2",
|
||
}
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.parent.mkdir(parents=True, exist_ok=True)
|
||
agent_path.touch()
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
|
||
)
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert "✅ Created: Dockerfile" in result.output
|
||
|
||
assert save_path.exists()
|
||
with open(save_path) as f:
|
||
dockerfile = f.read()
|
||
assert re.match("FROM langchain/langgraph-server:0.2-py3.*", dockerfile), (
|
||
"\n".join(dockerfile.splitlines()[:3])
|
||
)
|
||
|
||
|
||
def test_dockerfile_command_with_docker_compose() -> None:
|
||
"""Test the 'dockerfile' command with Docker Compose configuration."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"dependencies": ["./my_agent"],
|
||
"graphs": {"agent": "./my_agent/agent.py:graph"},
|
||
"env": ".env",
|
||
}
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
# Add agent.py file
|
||
agent_path = temp_dir / "my_agent" / "agent.py"
|
||
agent_path.parent.mkdir(parents=True, exist_ok=True)
|
||
agent_path.touch()
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"dockerfile",
|
||
str(save_path),
|
||
"--config",
|
||
str(temp_dir / "config.json"),
|
||
"--add-docker-compose",
|
||
],
|
||
)
|
||
|
||
# Assert command was successful
|
||
assert result.exit_code == 0
|
||
assert "✅ Created: Dockerfile" in result.output
|
||
assert "✅ Created: .dockerignore" in result.output
|
||
assert "✅ Created: docker-compose.yml" in result.output
|
||
assert (
|
||
"✅ Created: .env" in result.output or "➖ Skipped: .env" in result.output
|
||
)
|
||
assert "🎉 Files generated successfully" in result.output
|
||
|
||
# Check if Dockerfile, .dockerignore, docker-compose.yml, and .env were created
|
||
assert save_path.exists()
|
||
assert (temp_dir / ".dockerignore").exists()
|
||
assert (temp_dir / "docker-compose.yml").exists()
|
||
assert (temp_dir / ".env").exists() or "➖ Skipped: .env" in result.output
|
||
|
||
|
||
def test_dockerfile_command_with_bad_config() -> None:
|
||
"""Test the 'dockerfile' command with basic configuration."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"node_version": "20" # Add any other necessary configuration fields
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
["dockerfile", str(save_path), "--config", str(temp_dir / "conf.json")],
|
||
)
|
||
|
||
# Assert command was successful
|
||
assert result.exit_code == 2
|
||
assert "conf.json' does not exist" in result.output
|
||
|
||
|
||
def test_dockerfile_command_shows_wolfi_warning() -> None:
|
||
"""Test the 'dockerfile' command shows warning when image_distro is not wolfi."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.11",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
# No image_distro specified - should default to debian and show warning
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.touch()
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
|
||
)
|
||
|
||
# Assert command was successful
|
||
assert result.exit_code == 0, result.output
|
||
|
||
# Check that warning is shown
|
||
assert "Security Recommendation" in result.output
|
||
assert "Wolfi Linux" in result.output
|
||
assert "image_distro" in result.output
|
||
assert "wolfi" in result.output
|
||
|
||
|
||
def test_dockerfile_command_no_wolfi_warning_when_wolfi_set() -> None:
|
||
"""Test the 'dockerfile' command does NOT show warning when image_distro is wolfi."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.11",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
"image_distro": "wolfi", # Explicitly set to wolfi - should not show warning
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.touch()
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
|
||
)
|
||
|
||
# Assert command was successful
|
||
assert result.exit_code == 0, result.output
|
||
|
||
# Check that warning is NOT shown
|
||
assert "Security Recommendation" not in result.output
|
||
assert "Wolfi Linux" not in result.output
|
||
|
||
|
||
def test_build_command_shows_wolfi_warning() -> None:
|
||
"""Test the 'build' command shows warning when image_distro is not wolfi."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.11",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
# No image_distro specified - should default to debian and show warning
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.touch()
|
||
|
||
# Mock docker command since we don't want to actually build
|
||
with runner.isolated_filesystem():
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"build",
|
||
"--tag",
|
||
"test-image",
|
||
"--config",
|
||
str(temp_dir / "config.json"),
|
||
],
|
||
catch_exceptions=True,
|
||
)
|
||
|
||
# The command will fail because docker isn't available or we're mocking,
|
||
# but we should still see the warning before it fails
|
||
assert "Security Recommendation" in result.output
|
||
assert "Wolfi Linux" in result.output
|
||
assert "image_distro" in result.output
|
||
assert "wolfi" in result.output
|
||
|
||
|
||
def test_build_generate_proper_build_context():
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.11",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": [".", "../../..", "../.."],
|
||
"image_distro": "wolfi",
|
||
}
|
||
|
||
with temporary_config_folder(config_content, levels=3) as temp_dir:
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.touch()
|
||
|
||
# Mock docker command since we don't want to actually build
|
||
with runner.isolated_filesystem():
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"build",
|
||
"--tag",
|
||
"test-image",
|
||
"--config",
|
||
str(temp_dir / "config.json"),
|
||
],
|
||
catch_exceptions=True,
|
||
)
|
||
|
||
build_context_pattern = re.compile(r"--build-context\s+([\w-]+)=([^\s]+)")
|
||
|
||
build_contexts = re.findall(build_context_pattern, result.output)
|
||
assert len(build_contexts) == 2, (
|
||
f"Expected 2 build contexts, but found {len(build_contexts)}"
|
||
)
|
||
|
||
|
||
def test_dockerfile_command_with_api_version() -> None:
|
||
"""Test the 'dockerfile' command with --api-version flag."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.11",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.touch()
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"dockerfile",
|
||
str(save_path),
|
||
"--config",
|
||
str(temp_dir / "config.json"),
|
||
"--api-version",
|
||
"0.2.74",
|
||
],
|
||
)
|
||
|
||
# Assert command was successful
|
||
assert result.exit_code == 0, result.output
|
||
assert "✅ Created: Dockerfile" in result.output
|
||
|
||
# Check if Dockerfile was created and contains correct FROM line
|
||
assert save_path.exists()
|
||
with open(save_path) as f:
|
||
dockerfile = f.read()
|
||
assert "FROM langchain/langgraph-api:0.2.74-py3.11" in dockerfile
|
||
|
||
|
||
def test_dockerfile_command_with_api_version_and_base_image() -> None:
|
||
"""Test the 'dockerfile' command with both --api-version and --base-image flags."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.12",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
"image_distro": "wolfi",
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.touch()
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"dockerfile",
|
||
str(save_path),
|
||
"--config",
|
||
str(temp_dir / "config.json"),
|
||
"--api-version",
|
||
"1.0.0",
|
||
"--base-image",
|
||
"my-registry/custom-api",
|
||
],
|
||
)
|
||
|
||
# Assert command was successful
|
||
assert result.exit_code == 0, result.output
|
||
assert "✅ Created: Dockerfile" in result.output
|
||
|
||
# Check if Dockerfile was created and contains correct FROM line
|
||
assert save_path.exists()
|
||
with open(save_path) as f:
|
||
dockerfile = f.read()
|
||
assert "FROM my-registry/custom-api:1.0.0-py3.12-wolfi" in dockerfile
|
||
|
||
|
||
def test_dockerfile_command_with_api_version_nodejs() -> None:
|
||
"""Test the 'dockerfile' command with --api-version flag for Node.js config."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"node_version": "20",
|
||
"graphs": {"agent": "agent.js:graph"},
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
agent_path = temp_dir / "agent.js"
|
||
agent_path.touch()
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"dockerfile",
|
||
str(save_path),
|
||
"--config",
|
||
str(temp_dir / "config.json"),
|
||
"--api-version",
|
||
"0.2.74",
|
||
],
|
||
)
|
||
|
||
# Assert command was successful
|
||
assert result.exit_code == 0, result.output
|
||
assert "✅ Created: Dockerfile" in result.output
|
||
|
||
# Check if Dockerfile was created and contains correct FROM line
|
||
assert save_path.exists()
|
||
with open(save_path) as f:
|
||
dockerfile = f.read()
|
||
assert "FROM langchain/langgraphjs-api:0.2.74-node20" in dockerfile
|
||
|
||
|
||
def test_build_command_with_api_version() -> None:
|
||
"""Test the 'build' command with --api-version flag."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.11",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
"image_distro": "wolfi", # Use wolfi to avoid warning messages
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.touch()
|
||
|
||
# Mock docker command since we don't want to actually build
|
||
with runner.isolated_filesystem():
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"build",
|
||
"--tag",
|
||
"test-image",
|
||
"--config",
|
||
str(temp_dir / "config.json"),
|
||
"--api-version",
|
||
"0.2.74",
|
||
"--no-pull", # Avoid pulling non-existent images
|
||
],
|
||
catch_exceptions=True,
|
||
)
|
||
|
||
# Check that the build command is called with the correct tag
|
||
# The output should contain the docker build command with the api_version tag
|
||
assert "langchain/langgraph-api:0.2.74-py3.11-wolfi" in result.output
|
||
|
||
|
||
def test_build_command_with_api_version_and_base_image() -> None:
|
||
"""Test the 'build' command with both --api-version and --base-image flags."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.12",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
"image_distro": "wolfi", # Use wolfi to avoid warning messages
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.touch()
|
||
|
||
# Mock docker command since we don't want to actually build
|
||
with runner.isolated_filesystem():
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"build",
|
||
"--tag",
|
||
"test-image",
|
||
"--config",
|
||
str(temp_dir / "config.json"),
|
||
"--api-version",
|
||
"1.0.0",
|
||
"--base-image",
|
||
"my-registry/custom-api",
|
||
"--no-pull", # Avoid pulling non-existent images
|
||
],
|
||
catch_exceptions=True,
|
||
)
|
||
|
||
# Check that the build command includes the api_version
|
||
assert "my-registry/custom-api:1.0.0-py3.12-wolfi" in result.output
|
||
|
||
|
||
def test_prepare_args_and_stdin_with_api_version() -> None:
|
||
"""Test prepare_args_and_stdin function with api_version parameter."""
|
||
config_path = pathlib.Path(__file__).parent / "langgraph.json"
|
||
config = validate_config(
|
||
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
|
||
)
|
||
port = 8000
|
||
api_version = "0.2.74"
|
||
|
||
actual_args, actual_stdin = prepare_args_and_stdin(
|
||
capabilities=DEFAULT_DOCKER_CAPABILITIES,
|
||
config_path=config_path,
|
||
config=config,
|
||
docker_compose=None,
|
||
port=port,
|
||
watch=False,
|
||
api_version=api_version,
|
||
)
|
||
|
||
expected_args = [
|
||
"--project-directory",
|
||
str(pathlib.Path(__file__).parent.absolute()),
|
||
"-f",
|
||
"-",
|
||
]
|
||
|
||
# Check that the args are correct
|
||
assert actual_args == expected_args
|
||
|
||
# Check that the stdin contains the correct FROM line with api_version
|
||
assert "FROM langchain/langgraph-api:0.2.74-py3.11" in actual_stdin
|
||
|
||
|
||
def test_prepare_args_and_stdin_with_api_version_and_image() -> None:
|
||
"""Test prepare_args_and_stdin function with both api_version and image parameters."""
|
||
config_path = pathlib.Path(__file__).parent / "langgraph.json"
|
||
config = validate_config(
|
||
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
|
||
)
|
||
port = 8000
|
||
api_version = "0.2.74"
|
||
image = "my-custom-image:latest"
|
||
|
||
actual_args, actual_stdin = prepare_args_and_stdin(
|
||
capabilities=DEFAULT_DOCKER_CAPABILITIES,
|
||
config_path=config_path,
|
||
config=config,
|
||
docker_compose=None,
|
||
port=port,
|
||
watch=False,
|
||
api_version=api_version,
|
||
image=image,
|
||
)
|
||
|
||
# When image is provided, api_version should be ignored for the image
|
||
# but the stdin should not contain a build section (since image is provided)
|
||
assert "pull_policy: build" not in actual_stdin
|
||
|
||
|
||
def test_dockerfile_command_distributed_mode() -> None:
|
||
"""Test the 'dockerfile' command with --engine-runtime-mode distributed."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.11",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.touch()
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"dockerfile",
|
||
str(save_path),
|
||
"--config",
|
||
str(temp_dir / "config.json"),
|
||
"--engine-runtime-mode",
|
||
"distributed",
|
||
],
|
||
)
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert "✅ Created: Dockerfile" in result.output
|
||
|
||
assert save_path.exists()
|
||
with open(save_path) as f:
|
||
dockerfile = f.read()
|
||
assert "FROM langchain/langgraph-executor:3.11" in dockerfile
|
||
|
||
|
||
def test_dockerfile_command_combined_mode() -> None:
|
||
"""Test the 'dockerfile' command with --engine-runtime-mode combined_queue_worker."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.11",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.touch()
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"dockerfile",
|
||
str(save_path),
|
||
"--config",
|
||
str(temp_dir / "config.json"),
|
||
"--engine-runtime-mode",
|
||
"combined_queue_worker",
|
||
],
|
||
)
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert save_path.exists()
|
||
with open(save_path) as f:
|
||
dockerfile = f.read()
|
||
assert "FROM langchain/langgraph-api:3.11" in dockerfile
|
||
|
||
|
||
def test_dockerfile_command_distributed_with_explicit_base_image() -> None:
|
||
"""Test distributed mode with explicit --base-image overrides executor default."""
|
||
runner = CliRunner()
|
||
config_content = {
|
||
"python_version": "3.11",
|
||
"graphs": {"agent": "agent.py:graph"},
|
||
"dependencies": ["."],
|
||
}
|
||
|
||
with temporary_config_folder(config_content) as temp_dir:
|
||
save_path = temp_dir / "Dockerfile"
|
||
agent_path = temp_dir / "agent.py"
|
||
agent_path.touch()
|
||
|
||
result = runner.invoke(
|
||
cli,
|
||
[
|
||
"dockerfile",
|
||
str(save_path),
|
||
"--config",
|
||
str(temp_dir / "config.json"),
|
||
"--engine-runtime-mode",
|
||
"distributed",
|
||
"--base-image",
|
||
"my-custom-executor:latest",
|
||
],
|
||
)
|
||
|
||
assert result.exit_code == 0, result.output
|
||
assert save_path.exists()
|
||
with open(save_path) as f:
|
||
dockerfile = f.read()
|
||
assert "FROM my-custom-executor:latest" in dockerfile
|
||
|
||
|
||
def test_prepare_args_and_stdin_distributed_mode() -> None:
|
||
"""Test prepare_args_and_stdin with distributed mode includes all services."""
|
||
config_path = pathlib.Path(__file__).parent / "langgraph.json"
|
||
config = validate_config(
|
||
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
|
||
)
|
||
port = 8000
|
||
|
||
actual_args, actual_stdin = prepare_args_and_stdin(
|
||
capabilities=DEFAULT_DOCKER_CAPABILITIES,
|
||
config_path=config_path,
|
||
config=config,
|
||
docker_compose=None,
|
||
port=port,
|
||
watch=False,
|
||
engine_runtime_mode="distributed",
|
||
)
|
||
|
||
# API service should use langgraph-api base image
|
||
assert "FROM langchain/langgraph-api:" in actual_stdin
|
||
|
||
# Distributed mode sets N_JOBS_PER_WORKER=0 on the API service
|
||
assert 'N_JOBS_PER_WORKER: "0"' in actual_stdin
|
||
|
||
# Orchestrator service present
|
||
assert "langgraph-orchestrator:" in actual_stdin
|
||
|
||
# Executor service present with correct base image
|
||
assert "langgraph-executor:" in actual_stdin
|
||
assert "FROM langchain/langgraph-executor:" in actual_stdin
|
||
assert "executor_entrypoint.sh" in actual_stdin
|