Files
langgraph/libs/cli/tests/unit_tests/cli/test_cli.py
T
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>John Kennedy
43682f0830 chore(deps): bump the minor-and-patch group in /libs/cli with 4 updates (#7962)
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>&quot;&quot;</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 &gt; 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>&quot;&quot;</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 &gt; 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 &gt;= 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>
2026-06-03 07:42:36 +00:00

1332 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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