Merge remote-tracking branch 'origin/main' into nanobot-webui

This commit is contained in:
Xubin Ren
2026-04-19 05:15:27 +00:00
8 changed files with 340 additions and 46 deletions
+16 -4
View File
@@ -167,7 +167,19 @@ def test_stream_renderer_stop_for_input_stops_spinner():
spinner.stop.assert_called_once()
def test_make_console_uses_force_terminal():
"""Console should be created with force_terminal=True for proper ANSI handling."""
console = stream_mod._make_console()
assert console._force_terminal is True
def test_make_console_force_terminal_when_stdout_is_tty():
"""Console should set force_terminal=True when stdout is a TTY (rich output)."""
import sys
with patch.object(sys.stdout, "isatty", return_value=True):
console = stream_mod._make_console()
assert console._force_terminal is True
def test_make_console_force_terminal_false_when_stdout_is_not_tty():
"""Console should set force_terminal=False when stdout is not a TTY so that
ANSI escape codes (cursor visibility, braille spinner frames) don't pollute
piped output such as `docker exec -i` (#3265)."""
import sys
with patch.object(sys.stdout, "isatty", return_value=False):
console = stream_mod._make_console()
assert console._force_terminal is False
+33
View File
@@ -286,6 +286,39 @@ def test_find_by_name_accepts_camel_case_and_hyphen_aliases():
assert find_by_name("github-copilot").name == "github_copilot"
def test_config_explicit_xiaomi_mimo_provider_uses_default_api_base():
config = Config.model_validate(
{
"agents": {
"defaults": {
"provider": "xiaomi_mimo",
"model": "MiniMax-M1-80k",
}
},
"providers": {
"xiaomiMimo": {
"apiKey": "test-key",
}
},
}
)
assert config.get_provider_name() == "xiaomi_mimo"
assert config.get_api_base() == "https://api.xiaomimimo.com/v1"
def test_config_auto_detects_xiaomi_mimo_from_model_keyword():
config = Config.model_validate(
{
"agents": {"defaults": {"provider": "auto", "model": "mimo/MiniMax-M1-80k"}},
"providers": {"xiaomiMimo": {"apiKey": "test-key"}},
}
)
assert config.get_provider_name() == "xiaomi_mimo"
assert config.get_api_base() == "https://api.xiaomimimo.com/v1"
def test_config_auto_detects_ollama_from_local_api_base():
config = Config.model_validate(
{
+41 -1
View File
@@ -7,7 +7,6 @@ import pytest
from nanobot.agent.tools.cron import CronTool
from nanobot.cron.service import CronService
from nanobot.cron.types import CronJob, CronJobState, CronPayload, CronSchedule
from tests.test_openai_api import pytest_plugins
def _make_tool(tmp_path) -> CronTool:
@@ -346,6 +345,47 @@ def test_add_job_can_disable_delivery(tmp_path) -> None:
assert job.payload.deliver is False
def test_cron_schema_advertises_action_specific_requirements(tmp_path) -> None:
tool = _make_tool(tmp_path)
assert tool.parameters["required"] == ["action"]
assert tool.parameters["oneOf"] == [
{
"properties": {
"action": {"enum": ["add"]},
"message": {"type": "string", "minLength": 1},
},
"required": ["action", "message"],
},
{
"properties": {"action": {"enum": ["list"]}},
"required": ["action"],
},
{
"properties": {"action": {"enum": ["remove"]}},
"required": ["action", "job_id"],
},
]
def test_validate_params_requires_message_only_for_add(tmp_path) -> None:
tool = _make_tool(tmp_path)
assert "message is required when action='add'" in tool.validate_params({"action": "add"})
assert tool.validate_params({"action": "list"}) == []
assert "job_id is required when action='remove'" in tool.validate_params({"action": "remove"})
def test_add_job_empty_message_returns_actionable_error(tmp_path) -> None:
tool = _make_tool(tmp_path)
tool.set_context("telegram", "chat-1")
result = tool._add_job(None, "", 60, None, None, None)
assert "action='add' requires a non-empty 'message'" in result
assert "Retry including message=" in result
def test_list_excludes_disabled_jobs(tmp_path) -> None:
tool = _make_tool(tmp_path)
job = tool._cron.add_job(
+126 -1
View File
@@ -1,7 +1,8 @@
"""Tests for GitStore — line_ages() and core git operations."""
import subprocess
import time
from datetime import datetime, timezone, timedelta
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
import pytest
@@ -89,3 +90,127 @@ class TestLineAges:
# "- new" line and "- keep" line both age=0 (same day), but
# the key point is we get per-line results
assert len(ages) == 7
class TestNestedRepoProtection:
"""Regression tests for GitHub issue #2980: nested repo protection."""
def test_init_refuses_inside_git_repo(self, tmp_path):
"""init() should detect it's inside an existing git repo and refuse."""
project = tmp_path / "project"
project.mkdir()
(project / ".git").mkdir()
workspace = project / "workspace"
workspace.mkdir()
g = GitStore(workspace, tracked_files=["MEMORY.md"])
result = g.init()
assert result is False
assert not (workspace / ".git").is_dir()
def test_init_preserves_existing_gitignore(self, tmp_path):
"""init() should preserve existing .gitignore entries and append new ones."""
workspace = tmp_path / "workspace"
workspace.mkdir()
existing = "*.pyc\n__pycache__/\n"
(workspace / ".gitignore").write_text(existing, encoding="utf-8")
g = GitStore(workspace, tracked_files=["MEMORY.md"])
result = g.init()
assert result is True
gitignore = (workspace / ".gitignore").read_text(encoding="utf-8")
assert "*.pyc" in gitignore
assert "__pycache__/" in gitignore
assert "!MEMORY.md" in gitignore
assert "!.gitignore" in gitignore
def test_init_no_gitignore_creates_new(self, tmp_path):
"""init() should create .gitignore with Dream content when none exists."""
workspace = tmp_path / "workspace"
workspace.mkdir()
g = GitStore(workspace, tracked_files=["MEMORY.md"])
result = g.init()
assert result is True
gitignore = (workspace / ".gitignore").read_text(encoding="utf-8")
expected = g._build_gitignore()
assert gitignore == expected
def test_init_gitignore_merge_idempotent(self, tmp_path):
"""init() should not duplicate Dream entries already in .gitignore."""
workspace = tmp_path / "workspace"
workspace.mkdir()
# Pre-existing .gitignore that already has some Dream entries
existing = "*.pyc\n/*\n!MEMORY.md\n"
(workspace / ".gitignore").write_text(existing, encoding="utf-8")
g = GitStore(workspace, tracked_files=["MEMORY.md"])
result = g.init()
assert result is True
gitignore = (workspace / ".gitignore").read_text(encoding="utf-8")
# No duplicate lines
lines = gitignore.splitlines()
assert lines.count("/*") == 1
assert lines.count("!MEMORY.md") == 1
# Existing entry preserved, new Dream entries appended
assert "*.pyc" in gitignore
assert "!.gitignore" in gitignore
def test_init_outside_git_repo_works_normally(self, tmp_path):
"""init() should succeed and create .git when not inside a git repo."""
workspace = tmp_path / "workspace"
workspace.mkdir()
g = GitStore(workspace, tracked_files=["MEMORY.md"])
result = g.init()
assert result is True
assert (workspace / ".git").is_dir()
def test_init_refuses_inside_git_worktree(self, tmp_path):
"""init() should refuse when the parent checkout is a git worktree."""
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init", "-q", str(repo)], check=True)
(repo / "README.md").write_text("x\n", encoding="utf-8")
subprocess.run(["git", "-C", str(repo), "add", "README.md"], check=True)
subprocess.run(
[
"git",
"-C",
str(repo),
"-c",
"user.name=test",
"-c",
"user.email=test@example.com",
"commit",
"-q",
"-m",
"init",
],
check=True,
)
subprocess.run(["git", "-C", str(repo), "branch", "wt-branch"], check=True)
worktree = tmp_path / "worktree"
subprocess.run(
["git", "-C", str(repo), "worktree", "add", "-q", str(worktree), "wt-branch"],
check=True,
)
assert (worktree / ".git").is_file()
workspace = worktree / "workspace"
workspace.mkdir()
g = GitStore(workspace, tracked_files=["MEMORY.md"])
result = g.init()
assert result is False
assert not (workspace / ".git").exists()