a7d6d88f6f
CI / changes (push) Has been cancelled
CI / cd libs/checkpoint (push) Has been cancelled
CI / cd libs/checkpoint-conformance (push) Has been cancelled
CI / cd libs/checkpoint-postgres (push) Has been cancelled
CI / cd libs/checkpoint-sqlite (push) Has been cancelled
CI / cd libs/cli (push) Has been cancelled
CI / cd libs/prebuilt (push) Has been cancelled
CI / cd libs/sdk-py (push) Has been cancelled
CI / cd libs/langgraph (push) Has been cancelled
CI / Check SDK methods matching (push) Has been cancelled
CI / Check CLI schema hasn't changed #3.13 (push) Has been cancelled
CI / CLI integration test (push) Has been cancelled
CI / sdk-py integration test (push) Has been cancelled
CI / CI Success (push) Has been cancelled
baseline / benchmark (push) Has been cancelled
Deploy Redirects to GitHub Pages / deploy (push) Has been cancelled
170 lines
6.5 KiB
Python
170 lines
6.5 KiB
Python
"""Tests for `thread.values` — state-backed values projection."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
import httpx
|
|
|
|
from langgraph_sdk._async.http import HttpClient
|
|
from langgraph_sdk._async.threads import ThreadsClient
|
|
from streaming._events import (
|
|
lifecycle_completed_event,
|
|
lifecycle_started_event,
|
|
values_event,
|
|
)
|
|
from streaming._fake_server import FakeServer
|
|
|
|
|
|
async def test_values_subscribes_before_rest_fetch():
|
|
"""Values subscription is opened (values channel present in stream body)."""
|
|
fake = FakeServer()
|
|
fake.script([lifecycle_completed_event(seq=1)])
|
|
fake.set_state({"x": 1})
|
|
asgi = httpx.ASGITransport(app=fake.app)
|
|
async with httpx.AsyncClient(transport=asgi, base_url="http://test") as raw:
|
|
threads = ThreadsClient(HttpClient(raw))
|
|
async with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
await thread.run.start(input={})
|
|
results = []
|
|
async for snapshot in thread.values:
|
|
results.append(snapshot)
|
|
break # One snapshot is enough to verify subscription was opened.
|
|
|
|
# At least one stream request should contain "values" in its channels.
|
|
values_channel_seen = any(
|
|
"values" in body.get("channels", []) for body in fake.stream_request_bodies
|
|
)
|
|
assert values_channel_seen, (
|
|
f"Expected a stream request with 'values' channel, got: "
|
|
f"{fake.stream_request_bodies}"
|
|
)
|
|
|
|
|
|
async def test_values_first_yield_is_rest_state():
|
|
"""First item from `async for snapshot in thread.values` equals REST state values."""
|
|
fake = FakeServer()
|
|
fake.script([lifecycle_completed_event(seq=1)])
|
|
fake.set_state({"foo": "bar", "count": 42})
|
|
asgi = httpx.ASGITransport(app=fake.app)
|
|
async with httpx.AsyncClient(transport=asgi, base_url="http://test") as raw:
|
|
threads = ThreadsClient(HttpClient(raw))
|
|
async with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
await thread.run.start(input={})
|
|
first = None
|
|
async for snapshot in thread.values:
|
|
first = snapshot
|
|
break
|
|
|
|
assert first == {"foo": "bar", "count": 42}
|
|
assert fake.state_request_count >= 1
|
|
|
|
|
|
async def test_values_subsequent_yields_from_stream_events():
|
|
"""Items after the first come from live values stream events."""
|
|
fake = FakeServer()
|
|
fake.script(
|
|
[
|
|
lifecycle_started_event(seq=0),
|
|
values_event(seq=1, counter=1),
|
|
values_event(seq=2, counter=2),
|
|
lifecycle_completed_event(seq=3),
|
|
]
|
|
)
|
|
fake.set_state({"counter": 0})
|
|
asgi = httpx.ASGITransport(app=fake.app)
|
|
async with httpx.AsyncClient(transport=asgi, base_url="http://test") as raw:
|
|
threads = ThreadsClient(HttpClient(raw))
|
|
async with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
await thread.run.start(input={})
|
|
snapshots = []
|
|
async for snapshot in thread.values:
|
|
snapshots.append(snapshot)
|
|
|
|
# First snapshot is REST state values.
|
|
assert snapshots[0] == {"counter": 0}
|
|
# Subsequent snapshots are params.data from values events (full data dict).
|
|
assert {"counter": 1} in snapshots
|
|
assert {"counter": 2} in snapshots
|
|
|
|
|
|
async def test_values_completed_run_terminates():
|
|
"""Lifecycle completed causes `async for thread.values` to end cleanly."""
|
|
fake = FakeServer()
|
|
fake.script(
|
|
[
|
|
lifecycle_started_event(seq=0),
|
|
lifecycle_completed_event(seq=1),
|
|
]
|
|
)
|
|
fake.set_state({"done": True})
|
|
asgi = httpx.ASGITransport(app=fake.app)
|
|
async with httpx.AsyncClient(transport=asgi, base_url="http://test") as raw:
|
|
threads = ThreadsClient(HttpClient(raw))
|
|
async with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
await thread.run.start(input={})
|
|
snapshots = []
|
|
async for snapshot in thread.values:
|
|
snapshots.append(snapshot)
|
|
|
|
# Should have terminated without hanging; at least the REST snapshot.
|
|
assert len(snapshots) >= 1
|
|
assert snapshots[0] == {"done": True}
|
|
|
|
|
|
async def test_values_multiple_iterators_allowed():
|
|
"""Two concurrent `async for` loops on `thread.values` both yield independently."""
|
|
fake = FakeServer()
|
|
fake.script([lifecycle_completed_event(seq=1)])
|
|
fake.set_state({"shared": True})
|
|
asgi = httpx.ASGITransport(app=fake.app)
|
|
async with httpx.AsyncClient(transport=asgi, base_url="http://test") as raw:
|
|
threads = ThreadsClient(HttpClient(raw))
|
|
async with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
await thread.run.start(input={})
|
|
|
|
async def collect_first():
|
|
async for snapshot in thread.values:
|
|
return snapshot
|
|
|
|
result1, result2 = await asyncio.gather(collect_first(), collect_first())
|
|
|
|
assert result1 == {"shared": True}
|
|
assert result2 == {"shared": True}
|
|
|
|
|
|
async def test_values_await_delegates_to_output():
|
|
"""`await thread.values` returns the same result as `await thread.output`."""
|
|
fake = FakeServer()
|
|
fake.script([lifecycle_completed_event(seq=1)])
|
|
fake.set_state({"result": "terminal"})
|
|
asgi = httpx.ASGITransport(app=fake.app)
|
|
async with httpx.AsyncClient(transport=asgi, base_url="http://test") as raw:
|
|
threads = ThreadsClient(HttpClient(raw))
|
|
async with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
await thread.run.start(input={})
|
|
values_result = await thread.values
|
|
|
|
assert values_result == {"result": "terminal"}
|
|
|
|
|
|
async def test_values_no_historical_retention():
|
|
"""First snapshot is current REST state, not a cached/historical value."""
|
|
fake = FakeServer()
|
|
fake.script([lifecycle_completed_event(seq=1)])
|
|
# Set REST state to a specific value that can be verified as the source.
|
|
fake.set_state({"current": "state-v1"})
|
|
asgi = httpx.ASGITransport(app=fake.app)
|
|
async with httpx.AsyncClient(transport=asgi, base_url="http://test") as raw:
|
|
threads = ThreadsClient(HttpClient(raw))
|
|
async with threads.stream(thread_id="t-1", assistant_id="agent") as thread:
|
|
await thread.run.start(input={})
|
|
first = None
|
|
async for snapshot in thread.values:
|
|
first = snapshot
|
|
break
|
|
|
|
# First snapshot must be the REST state, not some cached prior value.
|
|
assert first == {"current": "state-v1"}
|
|
assert fake.state_request_count >= 1
|