import logging import pathlib import sys import time from urllib import error, request import langgraph_cli import langgraph_cli.config import langgraph_cli.docker from langgraph_cli.cli import prepare_args_and_stdin from langgraph_cli.constants import DEFAULT_PORT from langgraph_cli.exec import Runner, subp_exec from langgraph_cli.progress import Progress logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) def test(config: pathlib.Path, port: int, tag: str, verbose: bool): """Spin up API with Postgres/Redis via docker compose and wait until ready.""" logger.info("Starting test...") with Runner() as runner, Progress(message="Pulling...") as set: # Detect docker/compose capabilities capabilities = langgraph_cli.docker.check_capabilities(runner) # Validate config and prepare compose stdin/args using built image config_json = langgraph_cli.config.validate_config_file(config) args, stdin = prepare_args_and_stdin( capabilities=capabilities, config_path=config, config=config_json, docker_compose=None, port=port, watch=False, debugger_port=None, debugger_base_url=f"http://127.0.0.1:{port}", postgres_uri=None, api_version=None, image=tag, base_image=None, ) # Compose up with wait (implies detach), similar to `langgraph up --wait` args_up = [*args, "up", "--remove-orphans", "--wait"] compose_cmd = ["docker", "compose"] if capabilities.compose_type == "standalone": compose_cmd = ["docker-compose"] set("Starting...") try: runner.run( subp_exec( *compose_cmd, *args_up, input=stdin, verbose=verbose, ) ) except Exception as e: # noqa: BLE001 # On failure, show diagnostics then ensure clean teardown sys.stderr.write(f"docker compose up failed: {e}\n") try: sys.stderr.write("\n== docker compose ps ==\n") runner.run( subp_exec(*compose_cmd, *args, "ps", input=stdin, verbose=True) ) except Exception: pass try: sys.stderr.write("\n== docker compose logs (api) ==\n") runner.run( subp_exec( *compose_cmd, *args, "logs", "langgraph-api", input=stdin, verbose=True, ) ) except Exception: pass finally: try: runner.run( subp_exec( *compose_cmd, *args, "down", "-v", "--remove-orphans", input=stdin, verbose=False, ) ) finally: raise set("") base_url = f"http://localhost:{port}" ok_url = f"{base_url}/ok" logger.info(f"Waiting for {ok_url} to respond with 200...") deadline = time.time() + 30 last_err: Exception | None = None while time.time() < deadline: try: with request.urlopen(ok_url, timeout=2) as resp: if resp.status == 200: sys.stdout.write( f"""Ready!\n- API: {base_url}\n- /ok: 200 OK\n""" ) sys.stdout.flush() break else: last_err = RuntimeError(f"Unexpected status: {resp.status}") logger.error(f"Unexpected status: {resp.status}") except error.URLError as e: logger.error(f"URLError: {e}") last_err = e except Exception as e: # noqa: BLE001 logger.error(f"Exception: {e}") last_err = e time.sleep(0.5) else: logger.error("Timeout waiting for /ok to return 200") # Bring stack down before raising args_down = [*args, "down", "-v", "--remove-orphans"] try: runner.run( subp_exec( *compose_cmd, *args_down, input=stdin, verbose=verbose, ) ) finally: raise SystemExit( f"/ok did not return 202 within timeout. Last error: {last_err}" ) # Clean up: bring compose stack down to free ports for next test logger.info("Test succeeded. Bringing down compose stack...") try: args_down = [*args, "down", "-v", "--remove-orphans"] runner.run( subp_exec( *compose_cmd, *args_down, input=stdin, verbose=verbose, ) ) logger.info("Compose stack down. Finishing...") except Exception: logger.exception("Failed to bring down compose stack") pass logger.info("Test finished") if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument("-t", "--tag", type=str) parser.add_argument("-c", "--config", type=str, default="./langgraph.json") parser.add_argument("-p", "--port", type=int, default=DEFAULT_PORT) args = parser.parse_args() try: test(pathlib.Path(args.config), args.port, args.tag, verbose=True) except BaseException: logger.exception("Test failed") raise logger.info("Test execution finished")