|
|
|
|
@@ -333,10 +333,8 @@ def mock_agent_runtime(tmp_path):
|
|
|
|
|
"""Mock agent command dependencies for focused CLI tests."""
|
|
|
|
|
config = Config()
|
|
|
|
|
config.agents.defaults.workspace = str(tmp_path / "default-workspace")
|
|
|
|
|
cron_dir = tmp_path / "data" / "cron"
|
|
|
|
|
|
|
|
|
|
with patch("nanobot.config.loader.load_config", return_value=config) as mock_load_config, \
|
|
|
|
|
patch("nanobot.config.paths.get_cron_dir", return_value=cron_dir), \
|
|
|
|
|
patch("nanobot.cli.commands.sync_workspace_templates") as mock_sync_templates, \
|
|
|
|
|
patch("nanobot.cli.commands._make_provider", return_value=object()), \
|
|
|
|
|
patch("nanobot.cli.commands._print_agent_response") as mock_print_response, \
|
|
|
|
|
@@ -413,7 +411,6 @@ def test_agent_config_sets_active_path(monkeypatch, tmp_path: Path) -> None:
|
|
|
|
|
lambda path: seen.__setitem__("config_path", path),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
|
|
|
|
monkeypatch.setattr("nanobot.config.paths.get_cron_dir", lambda: config_file.parent / "cron")
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object())
|
|
|
|
|
@@ -438,6 +435,147 @@ def test_agent_config_sets_active_path(monkeypatch, tmp_path: Path) -> None:
|
|
|
|
|
assert seen["config_path"] == config_file.resolve()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_agent_uses_workspace_directory_for_cron_store(monkeypatch, tmp_path: Path) -> None:
|
|
|
|
|
config_file = tmp_path / "instance" / "config.json"
|
|
|
|
|
config_file.parent.mkdir(parents=True)
|
|
|
|
|
config_file.write_text("{}")
|
|
|
|
|
|
|
|
|
|
config = Config()
|
|
|
|
|
config.agents.defaults.workspace = str(tmp_path / "agent-workspace")
|
|
|
|
|
seen: dict[str, Path] = {}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object())
|
|
|
|
|
|
|
|
|
|
class _FakeCron:
|
|
|
|
|
def __init__(self, store_path: Path) -> None:
|
|
|
|
|
seen["cron_store"] = store_path
|
|
|
|
|
|
|
|
|
|
class _FakeAgentLoop:
|
|
|
|
|
def __init__(self, *args, **kwargs) -> None:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def process_direct(self, *_args, **_kwargs):
|
|
|
|
|
return OutboundMessage(channel="cli", chat_id="direct", content="ok")
|
|
|
|
|
|
|
|
|
|
async def close_mcp(self) -> None:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron)
|
|
|
|
|
monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None)
|
|
|
|
|
|
|
|
|
|
result = runner.invoke(app, ["agent", "-m", "hello", "-c", str(config_file)])
|
|
|
|
|
|
|
|
|
|
assert result.exit_code == 0
|
|
|
|
|
assert seen["cron_store"] == config.workspace_path / "cron" / "jobs.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_agent_workspace_override_does_not_migrate_legacy_cron(
|
|
|
|
|
monkeypatch, tmp_path: Path
|
|
|
|
|
) -> None:
|
|
|
|
|
config_file = tmp_path / "instance" / "config.json"
|
|
|
|
|
config_file.parent.mkdir(parents=True)
|
|
|
|
|
config_file.write_text("{}")
|
|
|
|
|
|
|
|
|
|
legacy_dir = tmp_path / "global" / "cron"
|
|
|
|
|
legacy_dir.mkdir(parents=True)
|
|
|
|
|
legacy_file = legacy_dir / "jobs.json"
|
|
|
|
|
legacy_file.write_text('{"jobs": []}')
|
|
|
|
|
|
|
|
|
|
override = tmp_path / "override-workspace"
|
|
|
|
|
config = Config()
|
|
|
|
|
seen: dict[str, Path] = {}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.config.paths.get_cron_dir", lambda: legacy_dir)
|
|
|
|
|
|
|
|
|
|
class _FakeCron:
|
|
|
|
|
def __init__(self, store_path: Path) -> None:
|
|
|
|
|
seen["cron_store"] = store_path
|
|
|
|
|
|
|
|
|
|
class _FakeAgentLoop:
|
|
|
|
|
def __init__(self, *args, **kwargs) -> None:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def process_direct(self, *_args, **_kwargs):
|
|
|
|
|
return OutboundMessage(channel="cli", chat_id="direct", content="ok")
|
|
|
|
|
|
|
|
|
|
async def close_mcp(self) -> None:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron)
|
|
|
|
|
monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None)
|
|
|
|
|
|
|
|
|
|
result = runner.invoke(
|
|
|
|
|
app,
|
|
|
|
|
["agent", "-m", "hello", "-c", str(config_file), "-w", str(override)],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result.exit_code == 0
|
|
|
|
|
assert seen["cron_store"] == override / "cron" / "jobs.json"
|
|
|
|
|
assert legacy_file.exists()
|
|
|
|
|
assert not (override / "cron" / "jobs.json").exists()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_agent_custom_config_workspace_does_not_migrate_legacy_cron(
|
|
|
|
|
monkeypatch, tmp_path: Path
|
|
|
|
|
) -> None:
|
|
|
|
|
config_file = tmp_path / "instance" / "config.json"
|
|
|
|
|
config_file.parent.mkdir(parents=True)
|
|
|
|
|
config_file.write_text("{}")
|
|
|
|
|
|
|
|
|
|
legacy_dir = tmp_path / "global" / "cron"
|
|
|
|
|
legacy_dir.mkdir(parents=True)
|
|
|
|
|
legacy_file = legacy_dir / "jobs.json"
|
|
|
|
|
legacy_file.write_text('{"jobs": []}')
|
|
|
|
|
|
|
|
|
|
custom_workspace = tmp_path / "custom-workspace"
|
|
|
|
|
config = Config()
|
|
|
|
|
config.agents.defaults.workspace = str(custom_workspace)
|
|
|
|
|
seen: dict[str, Path] = {}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.config.paths.get_cron_dir", lambda: legacy_dir)
|
|
|
|
|
|
|
|
|
|
class _FakeCron:
|
|
|
|
|
def __init__(self, store_path: Path) -> None:
|
|
|
|
|
seen["cron_store"] = store_path
|
|
|
|
|
|
|
|
|
|
class _FakeAgentLoop:
|
|
|
|
|
def __init__(self, *args, **kwargs) -> None:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def process_direct(self, *_args, **_kwargs):
|
|
|
|
|
return OutboundMessage(channel="cli", chat_id="direct", content="ok")
|
|
|
|
|
|
|
|
|
|
async def close_mcp(self) -> None:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron)
|
|
|
|
|
monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None)
|
|
|
|
|
|
|
|
|
|
result = runner.invoke(app, ["agent", "-m", "hello", "-c", str(config_file)])
|
|
|
|
|
|
|
|
|
|
assert result.exit_code == 0
|
|
|
|
|
assert seen["cron_store"] == custom_workspace / "cron" / "jobs.json"
|
|
|
|
|
assert legacy_file.exists()
|
|
|
|
|
assert not (custom_workspace / "cron" / "jobs.json").exists()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_agent_overrides_workspace_path(mock_agent_runtime):
|
|
|
|
|
workspace_path = Path("/tmp/agent-workspace")
|
|
|
|
|
|
|
|
|
|
@@ -544,7 +682,7 @@ def test_gateway_workspace_option_overrides_config(monkeypatch, tmp_path: Path)
|
|
|
|
|
assert config.workspace_path == override
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gateway_uses_config_directory_for_cron_store(monkeypatch, tmp_path: Path) -> None:
|
|
|
|
|
def test_gateway_uses_workspace_directory_for_cron_store(monkeypatch, tmp_path: Path) -> None:
|
|
|
|
|
config_file = tmp_path / "instance" / "config.json"
|
|
|
|
|
config_file.parent.mkdir(parents=True)
|
|
|
|
|
config_file.write_text("{}")
|
|
|
|
|
@@ -555,7 +693,6 @@ def test_gateway_uses_config_directory_for_cron_store(monkeypatch, tmp_path: Pat
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
|
|
|
|
monkeypatch.setattr("nanobot.config.paths.get_cron_dir", lambda: config_file.parent / "cron")
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object())
|
|
|
|
|
@@ -571,7 +708,130 @@ def test_gateway_uses_config_directory_for_cron_store(monkeypatch, tmp_path: Pat
|
|
|
|
|
result = runner.invoke(app, ["gateway", "--config", str(config_file)])
|
|
|
|
|
|
|
|
|
|
assert isinstance(result.exception, _StopGatewayError)
|
|
|
|
|
assert seen["cron_store"] == config_file.parent / "cron" / "jobs.json"
|
|
|
|
|
assert seen["cron_store"] == config.workspace_path / "cron" / "jobs.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gateway_workspace_override_does_not_migrate_legacy_cron(
|
|
|
|
|
monkeypatch, tmp_path: Path
|
|
|
|
|
) -> None:
|
|
|
|
|
config_file = tmp_path / "instance" / "config.json"
|
|
|
|
|
config_file.parent.mkdir(parents=True)
|
|
|
|
|
config_file.write_text("{}")
|
|
|
|
|
|
|
|
|
|
legacy_dir = tmp_path / "global" / "cron"
|
|
|
|
|
legacy_dir.mkdir(parents=True)
|
|
|
|
|
legacy_file = legacy_dir / "jobs.json"
|
|
|
|
|
legacy_file.write_text('{"jobs": []}')
|
|
|
|
|
|
|
|
|
|
override = tmp_path / "override-workspace"
|
|
|
|
|
config = Config()
|
|
|
|
|
seen: dict[str, Path] = {}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.session.manager.SessionManager", lambda _workspace: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.config.paths.get_cron_dir", lambda: legacy_dir)
|
|
|
|
|
|
|
|
|
|
class _StopCron:
|
|
|
|
|
def __init__(self, store_path: Path) -> None:
|
|
|
|
|
seen["cron_store"] = store_path
|
|
|
|
|
raise _StopGatewayError("stop")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("nanobot.cron.service.CronService", _StopCron)
|
|
|
|
|
|
|
|
|
|
result = runner.invoke(
|
|
|
|
|
app,
|
|
|
|
|
["gateway", "--config", str(config_file), "--workspace", str(override)],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert isinstance(result.exception, _StopGatewayError)
|
|
|
|
|
assert seen["cron_store"] == override / "cron" / "jobs.json"
|
|
|
|
|
assert legacy_file.exists()
|
|
|
|
|
assert not (override / "cron" / "jobs.json").exists()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gateway_custom_config_workspace_does_not_migrate_legacy_cron(
|
|
|
|
|
monkeypatch, tmp_path: Path
|
|
|
|
|
) -> None:
|
|
|
|
|
config_file = tmp_path / "instance" / "config.json"
|
|
|
|
|
config_file.parent.mkdir(parents=True)
|
|
|
|
|
config_file.write_text("{}")
|
|
|
|
|
|
|
|
|
|
legacy_dir = tmp_path / "global" / "cron"
|
|
|
|
|
legacy_dir.mkdir(parents=True)
|
|
|
|
|
legacy_file = legacy_dir / "jobs.json"
|
|
|
|
|
legacy_file.write_text('{"jobs": []}')
|
|
|
|
|
|
|
|
|
|
custom_workspace = tmp_path / "custom-workspace"
|
|
|
|
|
config = Config()
|
|
|
|
|
config.agents.defaults.workspace = str(custom_workspace)
|
|
|
|
|
seen: dict[str, Path] = {}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
|
|
|
|
monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.session.manager.SessionManager", lambda _workspace: object())
|
|
|
|
|
monkeypatch.setattr("nanobot.config.paths.get_cron_dir", lambda: legacy_dir)
|
|
|
|
|
|
|
|
|
|
class _StopCron:
|
|
|
|
|
def __init__(self, store_path: Path) -> None:
|
|
|
|
|
seen["cron_store"] = store_path
|
|
|
|
|
raise _StopGatewayError("stop")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("nanobot.cron.service.CronService", _StopCron)
|
|
|
|
|
|
|
|
|
|
result = runner.invoke(app, ["gateway", "--config", str(config_file)])
|
|
|
|
|
|
|
|
|
|
assert isinstance(result.exception, _StopGatewayError)
|
|
|
|
|
assert seen["cron_store"] == custom_workspace / "cron" / "jobs.json"
|
|
|
|
|
assert legacy_file.exists()
|
|
|
|
|
assert not (custom_workspace / "cron" / "jobs.json").exists()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migrate_cron_store_moves_legacy_file(tmp_path: Path) -> None:
|
|
|
|
|
"""Legacy global jobs.json is moved into the workspace on first run."""
|
|
|
|
|
from nanobot.cli.commands import _migrate_cron_store
|
|
|
|
|
|
|
|
|
|
legacy_dir = tmp_path / "global" / "cron"
|
|
|
|
|
legacy_dir.mkdir(parents=True)
|
|
|
|
|
legacy_file = legacy_dir / "jobs.json"
|
|
|
|
|
legacy_file.write_text('{"jobs": []}')
|
|
|
|
|
|
|
|
|
|
config = Config()
|
|
|
|
|
config.agents.defaults.workspace = str(tmp_path / "workspace")
|
|
|
|
|
workspace_cron = config.workspace_path / "cron" / "jobs.json"
|
|
|
|
|
|
|
|
|
|
with patch("nanobot.config.paths.get_cron_dir", return_value=legacy_dir):
|
|
|
|
|
_migrate_cron_store(config)
|
|
|
|
|
|
|
|
|
|
assert workspace_cron.exists()
|
|
|
|
|
assert workspace_cron.read_text() == '{"jobs": []}'
|
|
|
|
|
assert not legacy_file.exists()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migrate_cron_store_skips_when_workspace_file_exists(tmp_path: Path) -> None:
|
|
|
|
|
"""Migration does not overwrite an existing workspace cron store."""
|
|
|
|
|
from nanobot.cli.commands import _migrate_cron_store
|
|
|
|
|
|
|
|
|
|
legacy_dir = tmp_path / "global" / "cron"
|
|
|
|
|
legacy_dir.mkdir(parents=True)
|
|
|
|
|
(legacy_dir / "jobs.json").write_text('{"old": true}')
|
|
|
|
|
|
|
|
|
|
config = Config()
|
|
|
|
|
config.agents.defaults.workspace = str(tmp_path / "workspace")
|
|
|
|
|
workspace_cron = config.workspace_path / "cron" / "jobs.json"
|
|
|
|
|
workspace_cron.parent.mkdir(parents=True)
|
|
|
|
|
workspace_cron.write_text('{"new": true}')
|
|
|
|
|
|
|
|
|
|
with patch("nanobot.config.paths.get_cron_dir", return_value=legacy_dir):
|
|
|
|
|
_migrate_cron_store(config)
|
|
|
|
|
|
|
|
|
|
assert workspace_cron.read_text() == '{"new": true}'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gateway_uses_configured_port_when_cli_flag_is_missing(monkeypatch, tmp_path: Path) -> None:
|
|
|
|
|
|