chore: import upstream snapshot with attribution
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:00:43 +08:00
commit e4dcfc49aa
1668 changed files with 324490 additions and 0 deletions
View File
+215
View File
@@ -0,0 +1,215 @@
"""CLI smoke tests for the standalone ``deeptutor-cli`` package."""
from __future__ import annotations
import json
from typing import Any
import pytest
from typer.testing import CliRunner
from deeptutor.app import DeepTutorApp, TurnRequest
from deeptutor.runtime.bootstrap.builtin_capabilities import BUILTIN_CAPABILITY_CLASSES
from deeptutor_cli.main import app
runner = CliRunner()
def _install_fake_runtime(monkeypatch, captured_requests: list[TurnRequest]) -> None:
async def _start_turn(self, request): # noqa: ANN001
if isinstance(request, dict):
request = TurnRequest(**request)
captured_requests.append(request)
return {"id": request.session_id or "session-1"}, {"id": "turn-1"}
async def _stream_turn(self, turn_id: str, after_seq: int = 0): # noqa: ANN001
yield {"type": "session", "turn_id": turn_id, "seq": after_seq}
yield {"type": "stage_start", "stage": "responding"}
yield {"type": "content", "content": "response body"}
yield {"type": "result", "metadata": {"response": "response body"}}
yield {"type": "done"}
monkeypatch.setattr("deeptutor.app.facade.DeepTutorApp.start_turn", _start_turn)
monkeypatch.setattr("deeptutor.app.facade.DeepTutorApp.stream_turn", _stream_turn)
def test_run_command_json_mode(monkeypatch) -> None:
captured_requests: list[TurnRequest] = []
_install_fake_runtime(monkeypatch, captured_requests)
capabilities = list(BUILTIN_CAPABILITY_CLASSES)
for cap in capabilities:
result = runner.invoke(
app,
[
"run",
cap,
"hello world",
"--format",
"json",
"--tool",
"rag",
"--kb",
"demo-kb",
"--history-ref",
"session-old",
"--notebook-ref",
"nb1:rec1,rec2",
],
)
assert result.exit_code == 0, result.output
lines = [json.loads(line) for line in result.output.splitlines() if line.strip()]
assert any(line["type"] == "result" for line in lines)
assert len(captured_requests) == len(capabilities)
assert captured_requests[0].capability == "chat"
assert captured_requests[0].tools == ["rag"]
assert captured_requests[0].knowledge_bases == ["demo-kb"]
assert captured_requests[0].history_references == ["session-old"]
assert captured_requests[0].notebook_references == [
{"notebook_id": "nb1", "record_ids": ["rec1", "rec2"]}
]
assert {request.capability for request in captured_requests} == set(capabilities)
def test_builtin_capability_aliases_resolve_to_canonical_names() -> None:
runtime = DeepTutorApp()
assert runtime.resolve_capability("solve") == "deep_solve"
assert runtime.resolve_capability("quiz") == "deep_question"
assert runtime.resolve_capability("research") == "deep_research"
assert runtime.resolve_capability("viz") == "visualize"
assert runtime.resolve_capability("animate") == "math_animator"
assert runtime.resolve_capability("mastery") == "mastery_path"
with pytest.raises(ValueError, match="Unknown capability `auto`"):
runtime.resolve_capability("auto")
def test_run_command_rejects_removed_auto_capability() -> None:
result = runner.invoke(app, ["run", "auto", "hello"])
assert result.exit_code != 0
assert isinstance(result.exception, ValueError)
assert "Unknown capability `auto`" in str(result.exception)
def test_run_command_rich_mode(monkeypatch) -> None:
captured_requests: list[TurnRequest] = []
_install_fake_runtime(monkeypatch, captured_requests)
result = runner.invoke(app, ["run", "chat", "hello rich"])
assert result.exit_code == 0, result.output
assert "response body" in result.output
assert captured_requests[0].capability == "chat"
def test_run_command_with_config(monkeypatch) -> None:
captured_requests: list[TurnRequest] = []
_install_fake_runtime(monkeypatch, captured_requests)
result = runner.invoke(
app,
[
"run",
"deep_research",
"compare retrieval stacks",
"--config-json",
'{"mode":"report","depth":"deep"}',
],
)
assert result.exit_code == 0, result.output
request = captured_requests[0]
assert request.capability == "deep_research"
assert request.config == {
"mode": "report",
"depth": "deep",
}
def test_chat_repl_config_commands_match_docs_syntax(monkeypatch) -> None:
captured_requests: list[TurnRequest] = []
_install_fake_runtime(monkeypatch, captured_requests)
result = runner.invoke(
app,
["chat", "--config", "initial=true"],
input=(
"/config set num_questions 5\n"
'/config set question_types \'["short_answer","mcq"]\'\n'
"/refs\n"
"Generate questions\n"
"/quit\n"
),
)
assert result.exit_code == 0, result.output
assert '"num_questions": 5' in result.output
assert '"question_types"' in result.output
assert captured_requests[0].config == {
"initial": True,
"num_questions": 5,
"question_types": ["short_answer", "mcq"],
}
def test_chat_repl_backslash_continuation_sends_single_message(monkeypatch) -> None:
captured_requests: list[TurnRequest] = []
_install_fake_runtime(monkeypatch, captured_requests)
result = runner.invoke(
app,
["chat"],
input="Please review this code:\\\ndef fib(n): return n\n/quit\n",
)
assert result.exit_code == 0, result.output
assert captured_requests[0].content == "Please review this code:\ndef fib(n): return n"
def test_plugin_info_includes_capability_aliases_and_availability() -> None:
result = runner.invoke(app, ["plugin", "info", "deep_solve"])
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["name"] == "deep_solve"
assert payload["cli_aliases"] == ["solve"]
assert payload["availability"]["available"] is True
def test_session_list_command_uses_shared_store(monkeypatch) -> None:
async def _list_sessions(self, limit: int = 50, offset: int = 0): # noqa: ANN001
return [
{
"id": "session-1",
"title": "Algebra",
"capability": "chat",
"status": "completed",
"message_count": 4,
}
]
monkeypatch.setattr("deeptutor.app.facade.DeepTutorApp.list_sessions", _list_sessions)
result = runner.invoke(app, ["session", "list"])
assert result.exit_code == 0, result.output
assert "session-1" in result.output
assert "Algebra" in result.output
def test_start_command_delegates_to_runtime_launcher(monkeypatch) -> None:
calls: list[object] = []
def _fake_start(home=None): # noqa: ANN001
calls.append(home)
monkeypatch.setattr("deeptutor.runtime.launcher.start", _fake_start)
result = runner.invoke(app, ["start"])
assert result.exit_code == 0, result.output
assert calls == [None]
+16
View File
@@ -0,0 +1,16 @@
"""Unit tests for shared CLI helpers."""
from __future__ import annotations
import pytest
from deeptutor_cli.common import parse_json_object
def test_parse_json_object_whitespace_is_empty_dict() -> None:
assert parse_json_object(" ") == {}
def test_parse_json_object_invalid_json_still_errors() -> None:
with pytest.raises(ValueError, match="Invalid JSON config"):
parse_json_object("{")
+65
View File
@@ -0,0 +1,65 @@
"""CLI config command tests."""
from __future__ import annotations
from types import SimpleNamespace
from typer.testing import CliRunner
from deeptutor_cli.main import app
runner = CliRunner()
def test_config_show_handles_missing_embedding(monkeypatch) -> None:
"""CLI-only defaults skip embedding setup, so config show must not traceback."""
import deeptutor.services.config as config
monkeypatch.setattr(
config,
"load_system_settings",
lambda: {"backend_port": 8001, "frontend_port": 3782},
)
monkeypatch.setattr(
config,
"resolve_llm_runtime_config",
lambda: SimpleNamespace(
binding_hint="openai",
provider_name="openai",
provider_mode="standard",
model="gpt-4o-mini",
effective_url="https://api.openai.com/v1",
api_version=None,
extra_headers={},
api_key="",
),
)
def _missing_embedding() -> None:
raise ValueError("No active embedding model is configured.")
monkeypatch.setattr(config, "resolve_embedding_runtime_config", _missing_embedding)
monkeypatch.setattr(
config,
"resolve_search_runtime_config",
lambda: SimpleNamespace(
provider="duckduckgo",
requested_provider="duckduckgo",
status="ok",
fallback_reason=None,
base_url="",
proxy=None,
api_key="",
),
)
monkeypatch.setattr(
config,
"load_config_with_main",
lambda _name: {"system": {"language": "en"}, "tools": {}},
)
result = runner.invoke(app, ["config", "show"])
assert result.exit_code == 0, result.output
assert '"status": "not_configured"' in result.output
assert "No active embedding model is configured." in result.output
+124
View File
@@ -0,0 +1,124 @@
"""Contracts that keep the public docs aligned with the CLI surface."""
from __future__ import annotations
from pathlib import Path
import re
import shlex
ROOT = Path(__file__).resolve().parents[2]
DOCS_ROOT = ROOT / "site" / "src" / "content" / "docs"
PUBLIC_DOCS = (
ROOT / "README.md",
ROOT / "deeptutor_cli" / "README.md",
ROOT / "SKILL.md",
)
def _command_doc_paths() -> list[Path]:
paths: list[Path] = []
if DOCS_ROOT.exists():
paths.extend(DOCS_ROOT.rglob("*.md"))
paths.extend(path for path in PUBLIC_DOCS if path.exists())
return paths
def _docs_text() -> str:
return "\n".join(path.read_text(encoding="utf-8") for path in _command_doc_paths())
def _doc_ids() -> set[str]:
ids: set[str] = set()
for path in DOCS_ROOT.rglob("*.md"):
slug = path.relative_to(DOCS_ROOT).with_suffix("").as_posix()
ids.add(f"/{slug}")
ids.add(f"/{slug}/")
if slug.endswith("/index"):
base = slug[: -len("/index")]
ids.add(f"/{base}")
ids.add(f"/{base}/")
return ids
def _deeptutor_commands() -> list[str]:
commands: list[str] = []
pending = ""
for path in _command_doc_paths():
for line in path.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if not pending and not stripped.startswith("deeptutor "):
continue
continued = stripped.endswith("\\")
line_part = stripped[:-1].strip() if continued else stripped
pending = f"{pending} {line_part}".strip()
if continued:
continue
commands.append(pending)
pending = ""
return commands
def test_internal_docs_links_point_to_existing_pages() -> None:
ids = _doc_ids()
missing: list[tuple[str, str]] = []
for path in DOCS_ROOT.rglob("*.md"):
text = path.read_text(encoding="utf-8")
for match in re.finditer(r"\[[^\]]+\]\((/docs/[^)\s#]+)(?:#[^)]+)?\)", text):
href = match.group(1)
if href not in ids:
missing.append((str(path.relative_to(ROOT)), href))
assert missing == []
def test_documented_deeptutor_subcommands_exist() -> None:
top_level = {
"book",
"chat",
"config",
"init",
"kb",
"memory",
"notebook",
"partner",
"plugin",
"provider",
"run",
"serve",
"session",
"skill",
"start",
}
provider_subcommands = {"login"}
for command in _deeptutor_commands():
first_segment = command.split("|", 1)[0].split("#", 1)[0].strip()
if "<" in first_segment or "[" in first_segment:
continue
tokens = shlex.split(first_segment)
if len(tokens) < 2:
continue
assert tokens[1] in top_level, command
if tokens[1] == "provider" and len(tokens) >= 3:
assert tokens[2] in provider_subcommands, command
def test_deep_research_examples_include_required_config() -> None:
examples = [
command for command in _deeptutor_commands() if "deeptutor run deep_research" in command
]
assert examples, "docs should include at least one deep_research example"
for command in examples:
has_json_config = "--config-json" in command
has_pair_config = "--config mode=" in command and "--config depth=" in command
assert has_json_config or has_pair_config, command
def test_docs_do_not_advertise_removed_cli_forms() -> None:
text = _docs_text()
assert "deeptutor provider logout" not in text
assert "deeptutor memory show summary" not in text
assert "WS /api/v1/turns" not in text
+76
View File
@@ -0,0 +1,76 @@
from __future__ import annotations
from types import SimpleNamespace
class _FakeClient:
captured: list[dict] = []
def __init__(self, *, timeout: float):
self.timeout = timeout
def __enter__(self):
return self
def __exit__(self, *_args):
return None
def post(self, url: str, *, headers: dict, json: dict):
self.captured.append({"url": url, "headers": headers, "json": json})
return SimpleNamespace(status_code=200, text="")
def test_probe_llm_uses_max_completion_tokens_for_gpt5(monkeypatch) -> None:
from deeptutor_cli import init_wizard
_FakeClient.captured = []
monkeypatch.setattr(init_wizard.httpx, "Client", _FakeClient)
ok, _elapsed_ms, error = init_wizard.probe_llm(
base_url="https://example.test/v1",
api_key="sk-test",
binding="openai",
model="gpt-5-mini",
)
assert ok is True
assert error == ""
body = _FakeClient.captured[0]["json"]
assert body["max_completion_tokens"] == 1
assert "max_tokens" not in body
def test_probe_llm_keeps_max_tokens_for_legacy_chat_models(monkeypatch) -> None:
from deeptutor_cli import init_wizard
_FakeClient.captured = []
monkeypatch.setattr(init_wizard.httpx, "Client", _FakeClient)
init_wizard.probe_llm(
base_url="https://example.test/v1",
api_key="sk-test",
binding="openai",
model="gpt-3.5-turbo",
)
body = _FakeClient.captured[0]["json"]
assert body["max_tokens"] == 1
assert "max_completion_tokens" not in body
def test_probe_llm_keeps_anthropic_native_max_tokens(monkeypatch) -> None:
from deeptutor_cli import init_wizard
_FakeClient.captured = []
monkeypatch.setattr(init_wizard.httpx, "Client", _FakeClient)
init_wizard.probe_llm(
base_url="https://api.anthropic.test/v1",
api_key="sk-test",
binding="anthropic",
model="claude-sonnet-4",
)
body = _FakeClient.captured[0]["json"]
assert body["max_tokens"] == 1
assert "max_completion_tokens" not in body
+20
View File
@@ -0,0 +1,20 @@
from __future__ import annotations
from pathlib import Path
from deeptutor_cli.kb import _collect_documents
def test_collect_documents_from_directory_matches_uppercase_extensions(tmp_path: Path) -> None:
docs_dir = tmp_path / "资料"
docs_dir.mkdir()
upper_pdf = docs_dir / "报告.PDF"
upper_pdf.write_bytes(b"%PDF-1.4")
nested = docs_dir / "nested"
nested.mkdir()
upper_md = nested / "README.MD"
upper_md.write_text("hello", encoding="utf-8")
collected = [Path(path).name for path in _collect_documents([], str(docs_dir))]
assert collected == ["README.MD", "报告.PDF"]
+236
View File
@@ -0,0 +1,236 @@
"""CLI smoke tests for ``deeptutor notebook`` commands."""
from __future__ import annotations
from pathlib import Path
from typer.testing import CliRunner
from deeptutor_cli.main import app
runner = CliRunner()
class FakeNotebookManager:
"""Fake NotebookManager for CLI testing."""
def __init__(self) -> None:
self.notebooks: dict[str, dict] = {}
def create_notebook(self, name: str, description: str = "") -> dict:
import time
import uuid
nb = {
"id": str(uuid.uuid4())[:8],
"name": name,
"description": description,
"created_at": time.time(),
"updated_at": time.time(),
"records": [],
}
self.notebooks[nb["id"]] = nb
return nb
def get_notebook(self, notebook_id: str) -> dict | None:
return self.notebooks.get(notebook_id)
def add_record(
self,
notebook_ids: list[str],
record_type: str,
title: str,
user_query: str,
output: str,
summary: str = "",
metadata: dict | None = None,
kb_name: str | None = None,
) -> dict:
import time
import uuid
record = {
"id": str(uuid.uuid4())[:8],
"type": record_type,
"title": title,
"summary": summary,
"user_query": user_query,
"output": output,
"metadata": metadata or {},
"created_at": time.time(),
"kb_name": kb_name,
}
for nb_id in notebook_ids:
if nb_id in self.notebooks:
self.notebooks[nb_id]["records"].append(record)
return {"record": record, "added_to_notebooks": notebook_ids}
def update_record(self, notebook_id: str, record_id: str, **kwargs) -> dict | None:
nb = self.notebooks.get(notebook_id)
if not nb:
return None
for rec in nb["records"]:
if rec["id"] == record_id:
rec.update(kwargs)
return rec
return None
def remove_record(self, notebook_id: str, record_id: str) -> bool:
nb = self.notebooks.get(notebook_id)
if not nb:
return False
for i, rec in enumerate(nb["records"]):
if rec["id"] == record_id:
nb["records"].pop(i)
return True
return False
_fake_manager = FakeNotebookManager()
def _patch_facade(monkeypatch) -> None:
"""Patch DeepTutorApp methods to use the shared fake manager."""
def _create_notebook(self, **kwargs):
return _fake_manager.create_notebook(**kwargs)
def _get_notebook(self, notebook_id):
return _fake_manager.get_notebook(notebook_id)
def _add_record(self, **kwargs):
return _fake_manager.add_record(**kwargs)
def _update_record(self, notebook_id, record_id, **kwargs):
return _fake_manager.update_record(notebook_id, record_id, **kwargs)
def _remove_record(self, notebook_id, record_id) -> bool:
return _fake_manager.remove_record(notebook_id, record_id)
from deeptutor.app import facade
monkeypatch.setattr(facade.DeepTutorApp, "create_notebook", _create_notebook)
monkeypatch.setattr(facade.DeepTutorApp, "get_notebook", _get_notebook)
monkeypatch.setattr(facade.DeepTutorApp, "add_record", _add_record)
monkeypatch.setattr(facade.DeepTutorApp, "update_record", _update_record)
monkeypatch.setattr(facade.DeepTutorApp, "remove_record", _remove_record)
def test_notebook_add_md_creates_record(monkeypatch, tmp_path: Path) -> None:
"""add-md should read a markdown file and add it as a record to a notebook."""
_patch_facade(monkeypatch)
_fake_manager.notebooks.clear()
notebook = _fake_manager.create_notebook("Test Notebook")
md_file = tmp_path / "sample.md"
md_file.write_text("# Hello\n\nThis is a test.", encoding="utf-8")
result = runner.invoke(
app,
["notebook", "add-md", notebook["id"], str(md_file)],
)
assert result.exit_code == 0, result.output
assert "Added record" in result.output
stored = _fake_manager.get_notebook(notebook["id"])
assert stored is not None
assert len(stored["records"]) == 1
assert stored["records"][0]["title"] == "sample"
assert stored["records"][0]["type"] == "chat"
assert stored["records"][0]["output"] == "# Hello\n\nThis is a test."
def test_notebook_add_md_custom_title_and_type(monkeypatch, tmp_path: Path) -> None:
"""add-md should respect --title and --type options."""
_patch_facade(monkeypatch)
_fake_manager.notebooks.clear()
notebook = _fake_manager.create_notebook("Test Notebook")
md_file = tmp_path / "notes.md"
md_file.write_text("## Notes", encoding="utf-8")
result = runner.invoke(
app,
[
"notebook",
"add-md",
notebook["id"],
str(md_file),
"--title",
"My Custom Title",
"--type",
"research",
],
)
assert result.exit_code == 0, result.output
stored = _fake_manager.get_notebook(notebook["id"])
assert stored["records"][0]["title"] == "My Custom Title"
assert stored["records"][0]["type"] == "research"
def test_notebook_add_md_file_not_found(monkeypatch) -> None:
"""add-md should exit with an error when the file does not exist."""
_patch_facade(monkeypatch)
result = runner.invoke(
app,
["notebook", "add-md", "any-nb", "/path/to/missing.md"],
)
assert result.exit_code == 1
assert "File not found" in result.output
def test_notebook_replace_md_updates_record(monkeypatch, tmp_path: Path) -> None:
"""replace-md should replace the output content of an existing record."""
_patch_facade(monkeypatch)
_fake_manager.notebooks.clear()
notebook = _fake_manager.create_notebook("Test Notebook")
add_result = _fake_manager.add_record(
notebook_ids=[notebook["id"]],
record_type="chat",
title="Original",
user_query="",
output="Old content",
)
record_id = add_result["record"]["id"]
new_md = tmp_path / "updated.md"
new_md.write_text("# Updated\n\nNew content here.", encoding="utf-8")
result = runner.invoke(
app,
["notebook", "replace-md", notebook["id"], record_id, str(new_md)],
)
assert result.exit_code == 0, result.output
assert "Updated record" in result.output
stored = _fake_manager.get_notebook(notebook["id"])
assert len(stored["records"]) == 1
assert stored["records"][0]["output"] == "# Updated\n\nNew content here."
def test_notebook_replace_md_record_not_found(monkeypatch, tmp_path: Path) -> None:
"""replace-md should exit with an error when the record does not exist."""
_patch_facade(monkeypatch)
_fake_manager.notebooks.clear()
notebook = _fake_manager.create_notebook("Test Notebook")
md_file = tmp_path / "any.md"
md_file.write_text("content", encoding="utf-8")
result = runner.invoke(
app,
["notebook", "replace-md", notebook["id"], "nonexistent-record", str(md_file)],
)
assert result.exit_code == 1
assert "Record not found" in result.output
+35
View File
@@ -0,0 +1,35 @@
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[2]
PROVIDER_CMD = (ROOT / "deeptutor_cli" / "provider_cmd.py").read_text(encoding="utf-8")
CLI_README = (ROOT / "deeptutor_cli" / "README.md").read_text(encoding="utf-8")
ROOT_README = (ROOT / "README.md").read_text(encoding="utf-8")
class ProviderCliDocsContractTest(unittest.TestCase):
def test_provider_contract_describes_copilot_as_validation_not_oauth_login(self) -> None:
self.assertIn(
'help="Provider: openai-codex (OAuth login) | github-copilot (validate existing Copilot auth)"',
PROVIDER_CMD,
)
self.assertIn('"""Authenticate or validate provider access."""', PROVIDER_CMD)
self.assertIn("GitHub Copilot auth validation succeeded.", PROVIDER_CMD)
self.assertIn("GitHub Copilot auth validation failed:", PROVIDER_CMD)
self.assertNotIn("OAuth provider: openai-codex | github-copilot", PROVIDER_CMD)
self.assertNotIn("GitHub Copilot OAuth authentication succeeded.", PROVIDER_CMD)
def test_readmes_match_the_cli_contract(self) -> None:
self.assertIn(
"Provider auth (`openai-codex` OAuth login; `github-copilot` validates an existing Copilot auth session)",
ROOT_README,
)
self.assertIn(
"deeptutor provider login github-copilot # 校验现有 GitHub Copilot 认证是否可用",
CLI_README,
)
self.assertNotIn("OAuth login (`openai-codex`, `github-copilot`)", ROOT_README)
if __name__ == "__main__":
unittest.main()
+351
View File
@@ -0,0 +1,351 @@
"""Tests for the CLI turn-stream renderer against the chat-loop protocol.
The chat agent loop (deeptutor/agents/chat/agent_loop.py) streams every
round's text as ``content`` chunks with ``trace_kind=llm_chunk`` and labels
the round afterwards via a ``call_status`` marker carrying ``call_role``
(``narration`` | ``finish``). These tests feed that exact event shape into
the renderer and assert the terminal output (narration demoted to dim text
before its tool calls, finish rendered as the answer, no stray blank
progress lines) plus the ``ask_user`` pause/resume flow.
"""
from __future__ import annotations
import json
from typing import Any
from typer.testing import CliRunner
from deeptutor.app import TurnRequest
from deeptutor_cli import common as cli_common
from deeptutor_cli.common import _resolve_answer
from deeptutor_cli.main import app
runner = CliRunner()
def _chunk(call_id: str, text: str) -> dict[str, Any]:
return {
"type": "content",
"content": text,
"metadata": {
"call_id": call_id,
"call_kind": "agent_loop_round",
"trace_kind": "llm_chunk",
},
}
def _running(call_id: str, label: str = "Exploring") -> dict[str, Any]:
return {
"type": "progress",
"content": label,
"metadata": {
"call_id": call_id,
"call_kind": "agent_loop_round",
"trace_kind": "call_status",
"call_state": "running",
},
}
def _marker(call_id: str, role: str) -> dict[str, Any]:
return {
"type": "progress",
"content": "",
"metadata": {
"call_id": call_id,
"call_kind": "agent_loop_round",
"trace_kind": "call_status",
"call_state": "complete",
"call_role": role,
},
}
def _agent_loop_turn_events() -> list[dict[str, Any]]:
"""One narration round (with a tool call) followed by a finish round."""
return [
{"type": "stage_start", "stage": "responding", "source": "chat"},
_running("r1"),
_chunk("r1", "Let me check the knowledge base."),
_marker("r1", "narration"),
{
"type": "tool_call",
"content": "rag",
"metadata": {"args": {"query": "spectral clustering"}},
},
{
"type": "tool_result",
"content": "retrieved passage",
"metadata": {"tool": "rag"},
},
_running("r2"),
_chunk("r2", "The answer is **42**."),
_marker("r2", "finish"),
{"type": "stage_end", "stage": "responding", "source": "chat"},
{
"type": "result",
"metadata": {
"response": "The answer is **42**.",
"engine": "agent_loop",
"rounds": 2,
"tool_steps": 1,
"metadata": {
"cost_summary": {
"total_cost_usd": 0.0042,
"total_tokens": 12345,
"total_calls": 2,
}
},
},
},
{"type": "done"},
]
def _install_fake_runtime(
monkeypatch,
events: list[dict[str, Any]],
*,
replies: list[dict[str, Any]] | None = None,
) -> None:
async def _start_turn(self, request): # noqa: ANN001
if isinstance(request, dict):
request = TurnRequest(**request)
return {"id": request.session_id or "session-1"}, {"id": "turn-1"}
async def _stream_turn(self, turn_id: str, after_seq: int = 0): # noqa: ANN001
for event in events:
yield event
async def _submit_user_reply(self, turn_id, text=None, *, answers=None): # noqa: ANN001
if replies is not None:
replies.append({"turn_id": turn_id, "text": text, "answers": answers})
return True
monkeypatch.setattr("deeptutor.app.facade.DeepTutorApp.start_turn", _start_turn)
monkeypatch.setattr("deeptutor.app.facade.DeepTutorApp.stream_turn", _stream_turn)
monkeypatch.setattr("deeptutor.app.facade.DeepTutorApp.submit_user_reply", _submit_user_reply)
def test_narration_renders_before_tools_and_finish_is_answer(monkeypatch) -> None:
_install_fake_runtime(monkeypatch, _agent_loop_turn_events())
result = runner.invoke(app, ["run", "chat", "hello"])
assert result.exit_code == 0, result.output
narration_at = result.output.find("Let me check the knowledge base.")
tool_at = result.output.find("rag(")
answer_at = result.output.find("42")
assert narration_at != -1 and tool_at != -1 and answer_at != -1
# Narration belongs to the round BEFORE its tool calls; the finish
# round's text is the answer and comes last.
assert narration_at < tool_at < answer_at
# The chat wrapper stage emits no banner.
assert "▶ responding" not in result.output
def test_done_summary_line_includes_rounds_tools_tokens_cost(monkeypatch) -> None:
_install_fake_runtime(monkeypatch, _agent_loop_turn_events())
result = runner.invoke(app, ["run", "chat", "hello"])
assert result.exit_code == 0, result.output
assert "rounds=2" in result.output
assert "tools=1" in result.output
assert "tokens=12.3k" in result.output
assert "cost=$0.0042" in result.output
def test_empty_call_status_markers_print_nothing(monkeypatch) -> None:
_install_fake_runtime(monkeypatch, _agent_loop_turn_events())
result = runner.invoke(app, ["run", "chat", "hello"])
lines = result.output.splitlines()
# The two call_status markers carry empty content; no bare dim lines
# may leak out of them (a leaked line is whitespace-only).
assert not any(line.strip() == "" and line != "" for line in lines)
def test_thinking_chunks_collapse_to_single_indicator(monkeypatch) -> None:
events = _agent_loop_turn_events()
thinking = [
{
"type": "thinking",
"content": piece,
"metadata": {
"call_id": "r1",
"call_kind": "agent_loop_round",
"trace_kind": "llm_chunk",
},
}
for piece in ("First ", "I ", "should ", "look ", "things ", "up.")
]
events[2:2] = thinking # before r1's content chunk
_install_fake_runtime(monkeypatch, events)
result = runner.invoke(app, ["run", "chat", "hello"])
assert result.exit_code == 0, result.output
assert result.output.count("thinking…") == 1
# Raw reasoning text must not splatter into the transcript.
assert "should" not in result.output
def test_legacy_capability_content_still_renders(monkeypatch) -> None:
events = [
{"type": "stage_start", "stage": "planning", "source": "deep_research"},
{"type": "content", "content": "Plan text here."},
{"type": "stage_end", "stage": "planning", "source": "deep_research"},
{"type": "done"},
]
_install_fake_runtime(monkeypatch, events)
result = runner.invoke(app, ["run", "deep_research", "question"])
assert result.exit_code == 0, result.output
assert "▶ planning" in result.output
assert "Plan text here." in result.output
def _ask_user_events() -> list[dict[str, Any]]:
return [
{"type": "stage_start", "stage": "responding", "source": "chat"},
{
"type": "tool_result",
"content": "[awaiting user reply to: Which topic?]",
"metadata": {
"tool": "ask_user",
"tool_metadata": {
"ask_user": {
"intro": "Quick check",
"questions": [
{
"id": "q1",
"prompt": "Which topic?",
"options": [
{"label": "Algebra", "description": "Linear systems"},
{"label": "Geometry", "description": None},
],
"multi_select": False,
"allow_free_text": True,
}
],
}
},
},
},
_running("r2"),
_chunk("r2", "Algebra it is."),
_marker("r2", "finish"),
{"type": "stage_end", "stage": "responding", "source": "chat"},
{"type": "done"},
]
def test_ask_user_interactive_submits_selected_option(monkeypatch) -> None:
replies: list[dict[str, Any]] = []
_install_fake_runtime(monkeypatch, _ask_user_events(), replies=replies)
monkeypatch.setattr(cli_common, "_stdin_interactive", lambda: True)
monkeypatch.setattr(cli_common.console, "input", lambda prompt="": "1")
result = runner.invoke(app, ["run", "chat", "hello"])
assert result.exit_code == 0, result.output
assert "Which topic?" in result.output
assert "Algebra" in result.output
assert replies == [
{
"turn_id": "turn-1",
"text": None,
"answers": [{"questionId": "q1", "text": "Algebra"}],
}
]
def test_ask_user_non_interactive_sends_empty_reply(monkeypatch) -> None:
replies: list[dict[str, Any]] = []
_install_fake_runtime(monkeypatch, _ask_user_events(), replies=replies)
monkeypatch.setattr(cli_common, "_stdin_interactive", lambda: False)
result = runner.invoke(app, ["run", "chat", "hello"])
assert result.exit_code == 0, result.output
assert replies == [{"turn_id": "turn-1", "text": "", "answers": None}]
def test_ask_user_json_mode_sends_empty_reply_and_streams_events(monkeypatch) -> None:
replies: list[dict[str, Any]] = []
_install_fake_runtime(monkeypatch, _ask_user_events(), replies=replies)
result = runner.invoke(app, ["run", "chat", "hello", "--format", "json"])
assert result.exit_code == 0, result.output
lines = [json.loads(line) for line in result.output.splitlines() if line.strip()]
assert any(line.get("type") == "tool_result" for line in lines)
assert replies == [{"turn_id": "turn-1", "text": "", "answers": None}]
def test_terminator_llm_output_renders_after_buffered_chunks(monkeypatch) -> None:
events = [
{"type": "stage_start", "stage": "responding", "source": "chat"},
_chunk("r1", "Buffered narration."),
{
"type": "content",
"content": "Terminator final text.",
"metadata": {
"call_id": "chat-final-response-1",
"call_kind": "llm_final_response",
"trace_kind": "llm_output",
},
},
{"type": "stage_end", "stage": "responding", "source": "chat"},
{"type": "done"},
]
_install_fake_runtime(monkeypatch, events)
result = runner.invoke(app, ["run", "chat", "hello"])
assert result.exit_code == 0, result.output
buffered_at = result.output.find("Buffered narration.")
final_at = result.output.find("Terminator final text.")
assert buffered_at != -1 and final_at != -1
assert buffered_at < final_at
def test_resolve_answer_maps_numbers_to_labels() -> None:
labels = ["Algebra", "Geometry", "Calculus"]
assert _resolve_answer("2", labels, multi=False) == "Geometry"
assert _resolve_answer("free text", labels, multi=False) == "free text"
assert _resolve_answer("", labels, multi=False) == ""
assert _resolve_answer("1, 3", labels, multi=True) == "Algebra, Calculus"
assert _resolve_answer("1, custom", labels, multi=True) == "Algebra, custom"
# Out-of-range numbers fall through as text rather than crashing.
assert _resolve_answer("9", labels, multi=False) == "9"
def test_sources_render_compact_list(monkeypatch) -> None:
events = _agent_loop_turn_events()
events.insert(
-2,
{
"type": "sources",
"metadata": {
"sources": [
{"title": "Paper A", "url": "https://example.com/a"},
{"title": "Paper A", "url": "https://example.com/a"}, # dedupe
{"filename": "notes.pdf", "file_path": "/tmp/notes.pdf"},
]
},
},
)
_install_fake_runtime(monkeypatch, events)
result = runner.invoke(app, ["run", "chat", "hello"])
assert result.exit_code == 0, result.output
assert "sources (2):" in result.output
assert "Paper A" in result.output
assert "notes.pdf" in result.output