Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/finance/workflow/example_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "grpc_stubs"))

from deploy import deploy
from finance_agent import FinanceAgent
from market_agent import MarketResearchAgent
from agents.finance_agent import FinanceAgent
from agents.market_agent import MarketResearchAgent


def main(ticker: str = "AAPL"):
Expand Down
2 changes: 1 addition & 1 deletion examples/helloworld/workflow/example_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "grpc_stubs"))

from deploy import deploy
from example_agent import ExampleAgent
from agents.example_agent import ExampleAgent


def main(name: str = "World"):
Expand Down
8 changes: 4 additions & 4 deletions examples/portfolio/workflow/portfolio_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "grpc_stubs"))

from deploy import deploy
from intent_agent import IntentAgent
from metrics_agent import MetricsAgent
from risk_agent import RiskAgent
from advisor_agent import AdvisorAgent
from agents.intent_agent import IntentAgent
from agents.metrics_agent import MetricsAgent
from agents.risk_agent import RiskAgent
from agents.advisor_agent import AdvisorAgent


def main(
Expand Down
10 changes: 5 additions & 5 deletions examples/text2sql/workflow/text2sql_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "grpc_stubs"))

from deploy import deploy
from schema_agent import SchemaRetrievalAgent
from sql_generator_agent import SQLGeneratorAgent
from sql_validator_agent import SQLValidatorAgent
from sandbox_agent import SandboxExecutorAgent
from production_agent import ProductionExecutorAgent
from agents.schema_agent import SchemaRetrievalAgent
from agents.sql_generator_agent import SQLGeneratorAgent
from agents.sql_validator_agent import SQLValidatorAgent
from agents.sandbox_agent import SandboxExecutorAgent
from agents.production_agent import ProductionExecutorAgent


def main(question: str = "total order amount per customer region", n_candidates: int = 3):
Expand Down
9 changes: 3 additions & 6 deletions tests/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,10 @@ cd "$TEST_DIR"
echo ">> 1. Generating new project..."
ventis new-project $PROJECT_NAME
cd $PROJECT_NAME
grep -v 'gpu:' config/global_controller.yaml > config/global_controller.yaml.tmp
mv config/global_controller.yaml.tmp config/global_controller.yaml
grep -v 'gpu:' .car/config/global_controller.yaml > .car/config/global_controller.yaml.tmp
mv .car/config/global_controller.yaml.tmp .car/config/global_controller.yaml

echo ">> 2. Building agents (ventis build)..."
ventis build

echo ">> 3. Deploying workflow (ventis deploy)..."
echo ">> 2. Building and deploying workflow (ventis deploy)..."
ventis deploy &
DEPLOY_PID=$!

Expand Down
113 changes: 102 additions & 11 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ def _fake_controller_module(self, controller):

@patch("atexit.register")
@patch("signal.signal")
@patch("ventis.cli._run_build")
@patch("ventis.cli._ensure_grpc_stubs_importable")
@patch("ventis.cli._preflight_ec2_deploy")
def test_deploy_skips_ec2_preflight_for_local_config(
self,
preflight,
ensure_grpc,
_run_build,
_signal_patch,
_atexit_patch,
):
Expand All @@ -54,12 +56,14 @@ def test_deploy_skips_ec2_preflight_for_local_config(

@patch("atexit.register")
@patch("signal.signal")
@patch("ventis.cli._run_build")
@patch("ventis.cli._ensure_grpc_stubs_importable")
@patch("ventis.cli._preflight_ec2_deploy")
def test_deploy_runs_ec2_preflight_for_ec2_config(
self,
preflight,
ensure_grpc,
_run_build,
_signal_patch,
_atexit_patch,
):
Expand All @@ -81,6 +85,36 @@ def test_deploy_runs_ec2_preflight_for_ec2_config(
preflight.assert_called_once_with(config, os.getcwd())
controller.run.assert_called_once_with()

@patch("atexit.register")
@patch("signal.signal")
@patch("ventis.cli._run_build")
@patch("ventis.cli._ensure_grpc_stubs_importable")
@patch("ventis.cli._preflight_ec2_deploy")
def test_deploy_uses_car_when_present(
self, preflight, ensure_grpc, _run_build, _signal_patch, _atexit_patch
):
controller = MagicMock()
controller_module = self._fake_controller_module(controller)
args = SimpleNamespace(config=".car/config/global_controller.yaml")

with tempfile.TemporaryDirectory() as tmpdir, patch(
"ventis.cli.os.path.isfile", return_value=True
), patch(
"ventis.cli._load_config", return_value={"agents": []}
), patch.dict(
sys.modules, {"ventis.controller.global_controller": controller_module}
):
Path(tmpdir, ".car").mkdir()
cwd = os.getcwd()
os.chdir(tmpdir)
try:
cli.cmd_deploy(args)
finally:
os.chdir(cwd)

ensure_grpc.assert_called_once_with(os.path.join(os.path.realpath(tmpdir), ".car"))
preflight.assert_not_called()

@patch("ventis.cli._ensure_grpc_stubs_importable")
@patch("ventis.cli._require_docker_for_ec2")
def test_preflight_does_not_require_ssh_fields(self, require_docker, ensure_grpc):
Expand All @@ -103,28 +137,43 @@ class CliBuildTests(unittest.TestCase):
def _run_build(
self, project_dir, agent_yaml_paths, buildx_available, platform="linux/amd64"
):
"""Run cmd_build against project_dir with docker/subprocess calls mocked.
"""Run _run_build against project_dir with docker/subprocess calls mocked.

Returns (docker_calls, generate_docker_mock, generate_workflow_docker_mock).
"""
config_path = project_dir / "config" / "global_controller.yaml"
args = SimpleNamespace(config=str(config_path))
artifact_root = (
project_dir / ".car" if (project_dir / ".car").is_dir() else project_dir
)
config_path = artifact_root / "config" / "global_controller.yaml"
docker_calls = []

def fake_run(cmd, check):
docker_calls.append(cmd)
return SimpleNamespace(returncode=0)

def fake_glob(pattern):
if pattern.endswith("*.proto"):
return ["proto/a.proto"]
if agent_yaml_paths:
self.assertEqual(
os.path.realpath(Path(pattern).parent),
os.path.realpath(Path(agent_yaml_paths[0]).parent),
)
return agent_yaml_paths

def fake_generate_stub(yaml_path, _output_path):
with open(yaml_path) as f:
self.assertIn("agent", yaml.safe_load(f))

with (
patch(
"ventis.cli._get_package_dir",
return_value=str(project_dir / "package"),
),
patch("ventis.cli.glob.glob", side_effect=fake_glob),
patch(
"ventis.cli.glob.glob",
side_effect=[agent_yaml_paths, ["proto/a.proto"]],
"ventis.stub_generator.generate_stub", side_effect=fake_generate_stub
),
patch("ventis.stub_generator.generate_stub"),
patch("ventis.stub_generator.generate_docker") as generate_docker,
patch(
"ventis.stub_generator.generate_workflow_docker"
Expand All @@ -136,7 +185,7 @@ def fake_run(cmd, check):
cwd = os.getcwd()
os.chdir(project_dir)
try:
cli.cmd_build(args)
cli._run_build(str(config_path))
finally:
os.chdir(cwd)

Expand Down Expand Up @@ -240,6 +289,32 @@ def test_build_uses_buildx_bake_when_available(self):
)
self.assertEqual(targets["workflow"]["tags"], ["ventis-workflow"])

def test_build_uses_car_when_present(self):
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir)
artifact_root = project_dir / ".car"
source_root = artifact_root / "app"
source_root.mkdir(parents=True)
source_yaml = self._write_agent_and_workflow_config(source_root)
source_root.joinpath("config").rename(artifact_root / "config")
agent_yaml = artifact_root / "config" / source_yaml.name
source_yaml.rename(agent_yaml)

manifest = artifact_root / "config" / "global_controller.yaml"
_, generate_docker, generate_workflow_docker = self._run_build(
project_dir, [str(manifest), str(agent_yaml)], buildx_available=True
)

for call in (generate_docker, generate_workflow_docker):
self.assertEqual(
os.path.realpath(call.call_args.kwargs["project_dir"]),
os.path.realpath(source_root),
)
self.assertEqual(
os.path.realpath(generate_docker.call_args.kwargs["output_dir"]),
os.path.realpath(artifact_root / "docker_container" / "ExampleAgent"),
)

def test_build_with_no_agents_builds_nothing(self):
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir)
Expand All @@ -252,7 +327,7 @@ def test_build_with_no_agents_builds_nothing(self):

self.assertFalse(any(call[0] == "docker" for call in docker_calls))

def test_build_skips_agent_without_entrypoint(self):
def test_build_fails_when_stub_cannot_be_generated(self):
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir)
(project_dir / "config").mkdir()
Expand All @@ -268,9 +343,8 @@ def test_build_skips_agent_without_entrypoint(self):
)
)

docker_calls, _, _ = self._run_build(project_dir, [], buildx_available=True)

self.assertFalse(any(call[0] == "docker" for call in docker_calls))
with self.assertRaises(SystemExit):
self._run_build(project_dir, [], buildx_available=True)

def _write_requirements_config(self, project_dir):
"""Scaffold one plain agent, one agent with `requirements`, one workflow with `requirements`."""
Expand Down Expand Up @@ -373,5 +447,22 @@ def test_build_ignores_non_list_requirements(self):
self.assertEqual(generate_docker.call_args.kwargs["requirements"], [])


class CliCleanTests(unittest.TestCase):
def test_clean_uses_car_when_present(self):
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir)
(project_dir / ".car" / "stubs").mkdir(parents=True)
(project_dir / "stubs").mkdir()
cwd = os.getcwd()
os.chdir(project_dir)
try:
cli.cmd_clean(SimpleNamespace())
finally:
os.chdir(cwd)

self.assertFalse((project_dir / ".car" / "stubs").exists())
self.assertTrue((project_dir / "stubs").exists())


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion tests/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

import ventis.deploy as deploy_module
import ventis.controller.deploy as deploy_module


class _FakeRedis:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_error_propagation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from ventis.controller.local_controller import LocalController
from ventis.controller.local_controller_frontend import LocalControllerServicer
from ventis.future import Future
from ventis.controller.future import Future
import local_controler_pb2


Expand Down
4 changes: 2 additions & 2 deletions tests/test_future.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
),
)

import ventis.future as future_module
import ventis.ventis_context as ventis_context
import ventis.controller.future as future_module
import ventis.controller.ventis_context as ventis_context


class _FakeRedis:
Expand Down
8 changes: 5 additions & 3 deletions tests/test_global_controller_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,16 @@ def test_a_second_call_with_a_new_config_overwrites_the_published_value(self):
},
)

def test_missing_project_id_or_database_publishes_safe_defaults(self):
controller = _bare_controller({})
def test_missing_database_publishes_safe_default(self):
# project_id is always populated by _load_config() by the time _write_identity()
# runs -- only database_url has a real "unset" case to default here.
controller = _bare_controller({"project_id": "11111111-1111-1111-1111-111111111111"})

controller._write_identity()

self.assertEqual(
controller.redis.hgetall(GlobalController.IDENTITY_KEY),
{"project_id": "0", "database_url": ""},
{"project_id": "11111111-1111-1111-1111-111111111111", "database_url": ""},
)


Expand Down
70 changes: 70 additions & 0 deletions tests/test_global_controller_project_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""_load_config() must mint a project_id when a config file omits one, and persist it back
to the file so the same value survives a reload_config() or process restart -- not a fresh
uuid on every load.
"""

import os
import re
import sys
import tempfile
import unittest

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

import yaml

from ventis.controller.global_controller import GlobalController

UUID_HEX_RE = re.compile(r"^[0-9a-f]{32}$")


def _write_config(body):
f = tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False)
f.write(body)
f.close()
return f.name


class LoadConfigProjectIdTests(unittest.TestCase):
def test_generates_and_persists_project_id_when_missing(self):
config_path = _write_config("agents: []\npoll_interval: 5\n")
try:
config = GlobalController._load_config(config_path)

self.assertTrue(UUID_HEX_RE.match(config["project_id"]))

with open(config_path) as f:
on_disk = yaml.safe_load(f)
self.assertEqual(on_disk["project_id"], config["project_id"])
finally:
os.unlink(config_path)

def test_reload_reuses_the_persisted_project_id_instead_of_minting_a_new_one(self):
config_path = _write_config("agents: []\npoll_interval: 5\n")
try:
first = GlobalController._load_config(config_path)
second = GlobalController._load_config(config_path)

self.assertEqual(first["project_id"], second["project_id"])
finally:
os.unlink(config_path)

def test_existing_project_id_is_left_untouched(self):
config_path = _write_config(
'agents: []\nproject_id: "11111111-1111-1111-1111-111111111111"\n'
)
try:
config = GlobalController._load_config(config_path)

self.assertEqual(config["project_id"], "11111111-1111-1111-1111-111111111111")

with open(config_path) as f:
contents = f.read()
# No second project_id line got appended alongside the existing one.
self.assertEqual(contents.count("project_id"), 1)
finally:
os.unlink(config_path)


if __name__ == "__main__":
unittest.main()
Loading