This commit is contained in:
@@ -1,11 +1,22 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from tai.cli import app
|
||||
from tai.cli import (
|
||||
_download_markdown_url,
|
||||
_inject_url_credentials,
|
||||
_load_env_file,
|
||||
_materialize_runbook_add_path,
|
||||
_materialize_runbooks_sync_path,
|
||||
_resolve_secret,
|
||||
app,
|
||||
)
|
||||
from tai.collectors import CollectedItem, CollectionReport
|
||||
from tai.rag_retriever import Chunk, EmbeddedChunk
|
||||
from tai.ssh_client import SSHCommandResult
|
||||
@@ -344,7 +355,7 @@ def test_interactive_rag_debug_prints_retrieval_scores(monkeypatch) -> None: #
|
||||
|
||||
def test_history_command_lists_sessions(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
class FakeStore:
|
||||
def __init__(self, _path: str) -> None:
|
||||
def __init__(self, _path: str, **_kwargs) -> None:
|
||||
pass
|
||||
|
||||
def list_recent(self, *, host: str | None = None, limit: int = 20):
|
||||
@@ -360,12 +371,12 @@ def test_history_command_lists_sessions(monkeypatch) -> None: # type: ignore[no
|
||||
]
|
||||
return []
|
||||
|
||||
monkeypatch.setattr("tai.cli.SessionStore", FakeStore)
|
||||
monkeypatch.setattr("tai.cli.RunHistoryStore", FakeStore)
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["history", "--session-memory", "~/.tai/sessions", "--host", "web01"],
|
||||
["history", "--history-db", "~/.tai/history.db", "--host", "web01"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
@@ -375,7 +386,7 @@ def test_history_command_lists_sessions(monkeypatch) -> None: # type: ignore[no
|
||||
|
||||
def test_history_command_exports_markdown(monkeypatch, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
||||
class FakeStore:
|
||||
def __init__(self, _path: str) -> None:
|
||||
def __init__(self, _path: str, **_kwargs) -> None:
|
||||
pass
|
||||
|
||||
def list_recent(self, *, host: str | None = None, limit: int = 20):
|
||||
@@ -389,13 +400,13 @@ def test_history_command_exports_markdown(monkeypatch, tmp_path: Path) -> None:
|
||||
)
|
||||
]
|
||||
|
||||
monkeypatch.setattr("tai.cli.SessionStore", FakeStore)
|
||||
monkeypatch.setattr("tai.cli.RunHistoryStore", FakeStore)
|
||||
export_path = tmp_path / "history.md"
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["history", "--session-memory", "~/.tai/sessions", "--export", str(export_path)],
|
||||
["history", "--history-db", "~/.tai/history.db", "--export", str(export_path)],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
@@ -440,11 +451,12 @@ def test_interactive_history_without_store_shows_hint(monkeypatch) -> None: # t
|
||||
"5566",
|
||||
"--no-probe",
|
||||
"--interactive",
|
||||
"--no-history",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Session memory is disabled" in result.stdout
|
||||
assert "History DB is disabled" in result.stdout
|
||||
|
||||
|
||||
def test_run_analyze_writes_output_file(monkeypatch, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
||||
@@ -467,9 +479,10 @@ def test_run_analyze_writes_output_file(monkeypatch, tmp_path: Path) -> None: #
|
||||
)
|
||||
|
||||
monkeypatch.setattr("tai.cli.collect_from_plan", fake_collect_from_plan)
|
||||
response = SimpleNamespace(content="Root Cause\n\nEvidence\n\nRecommended Actions")
|
||||
monkeypatch.setattr(
|
||||
"tai.cli.AIClient.complete",
|
||||
lambda *_args, **_kwargs: SimpleNamespace(content="Root Cause\n\nEvidence\n\nRecommended Actions"),
|
||||
lambda *_args, **_kwargs: response,
|
||||
)
|
||||
|
||||
output_path = tmp_path / "analysis.md"
|
||||
@@ -543,7 +556,304 @@ def test_run_analyze_writes_json_output_and_strips_ansi(monkeypatch, tmp_path: P
|
||||
|
||||
assert result.exit_code == 0
|
||||
payload = json.loads(output_path.read_text(encoding="utf-8"))
|
||||
assert payload["schema"] == "tai.analysis.v1"
|
||||
assert "generated_at" in payload
|
||||
assert payload["issue"] == "apache failed"
|
||||
assert payload["host"] == "ssh.archflux.net"
|
||||
assert payload["collection"] == {"total": 1, "failed": 0, "succeeded": 1}
|
||||
assert payload["token_usage"] == {
|
||||
"prompt_tokens": None,
|
||||
"completion_tokens": None,
|
||||
"total_tokens": None,
|
||||
}
|
||||
assert "Root Cause" in payload["analysis"]
|
||||
assert "\u001b" not in payload["analysis"]
|
||||
|
||||
|
||||
def test_run_analyze_writes_history_db_record(monkeypatch, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
||||
_mock_session(monkeypatch)
|
||||
|
||||
async def fake_collect_from_plan(_session, _plan) -> CollectionReport: # type: ignore[no-untyped-def]
|
||||
return CollectionReport(
|
||||
host="ssh.archflux.net",
|
||||
items=[
|
||||
CollectedItem(
|
||||
name="kernel",
|
||||
result=SSHCommandResult(
|
||||
command="uname -a",
|
||||
exit_code=0,
|
||||
stdout="Linux test",
|
||||
stderr="",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.setattr("tai.cli.collect_from_plan", fake_collect_from_plan)
|
||||
response = SimpleNamespace(content="Root Cause\n\nEvidence\n\nRecommended Actions")
|
||||
monkeypatch.setattr(
|
||||
"tai.cli.AIClient.complete",
|
||||
lambda *_args, **_kwargs: response,
|
||||
)
|
||||
|
||||
history_db = tmp_path / "history.db"
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"run", "apache failed",
|
||||
"--host",
|
||||
"ssh.archflux.net",
|
||||
"--port",
|
||||
"5566",
|
||||
"--no-probe",
|
||||
"--analyze",
|
||||
"--history-db",
|
||||
str(history_db),
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
import sqlite3
|
||||
|
||||
with sqlite3.connect(str(history_db)) as conn:
|
||||
row = conn.execute(
|
||||
"SELECT host, issue, payload_json FROM run_history ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
|
||||
assert row is not None
|
||||
assert row[0] == "ssh.archflux.net"
|
||||
assert row[1] == "apache failed"
|
||||
payload = json.loads(row[2])
|
||||
assert payload["schema"] == "tai.analysis.v1"
|
||||
assert payload["host"] == "ssh.archflux.net"
|
||||
|
||||
|
||||
def test_materialize_runbooks_sync_path_http_webroot(monkeypatch, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
||||
html = '<html><body><a href="nginx.md">nginx</a><a href="ssh.md">ssh</a></body></html>'
|
||||
|
||||
def fake_download(url: str) -> str:
|
||||
if url == "https://kb.example/runbooks/":
|
||||
return html
|
||||
if url.endswith("nginx.md"):
|
||||
return "---\nservice: nginx\n---\nbody"
|
||||
if url.endswith("ssh.md"):
|
||||
return "---\nservice: ssh\n---\nbody"
|
||||
raise AssertionError(url)
|
||||
|
||||
monkeypatch.setattr("tai.cli._download_text_url", fake_download)
|
||||
source_dir, label, temp_dir = _materialize_runbooks_sync_path(
|
||||
"https://kb.example/runbooks/",
|
||||
identity_file=None,
|
||||
jump_host=None,
|
||||
ignore_ssh_config=False,
|
||||
)
|
||||
|
||||
assert label == "https://kb.example/runbooks/"
|
||||
assert temp_dir is not None
|
||||
assert (source_dir / "nginx.md").is_file()
|
||||
assert (source_dir / "ssh.md").is_file()
|
||||
|
||||
|
||||
def test_materialize_runbook_add_path_http_url(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
monkeypatch.setattr(
|
||||
"tai.cli._download_markdown_url",
|
||||
lambda _url: "---\nservice: nginx\n---\nbody",
|
||||
)
|
||||
source_file, label, temp_dir = _materialize_runbook_add_path(
|
||||
"https://kb.example/runbooks/nginx.md",
|
||||
identity_file=None,
|
||||
jump_host=None,
|
||||
ignore_ssh_config=False,
|
||||
)
|
||||
|
||||
assert label == "https://kb.example/runbooks/nginx.md"
|
||||
assert temp_dir is not None
|
||||
assert source_file.name == "nginx.md"
|
||||
assert source_file.read_text(encoding="utf-8").startswith("---")
|
||||
|
||||
|
||||
def test_download_markdown_url_rejects_html(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
monkeypatch.setattr(
|
||||
"tai.cli._download_text_url",
|
||||
lambda _url: "<!DOCTYPE html><html><body>not markdown</body></html>",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="does not appear to be a Markdown payload"):
|
||||
_download_markdown_url("https://kb.example/runbooks/nginx.md")
|
||||
|
||||
|
||||
def test_materialize_runbooks_sync_path_http_skips_html_wrappers(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
html = '<html><body><a href="nginx.md">nginx</a><a href="ssh.md">ssh</a></body></html>'
|
||||
|
||||
def fake_download(url: str) -> str:
|
||||
if url == "https://kb.example/runbooks/":
|
||||
return html
|
||||
if url.endswith("nginx.md"):
|
||||
return "---\nservice: nginx\n---\nbody"
|
||||
if url.endswith("ssh.md"):
|
||||
return "<!DOCTYPE html><html><body>wrapper</body></html>"
|
||||
raise AssertionError(url)
|
||||
|
||||
monkeypatch.setattr("tai.cli._download_text_url", fake_download)
|
||||
source_dir, _label, temp_dir = _materialize_runbooks_sync_path(
|
||||
"https://kb.example/runbooks/",
|
||||
identity_file=None,
|
||||
jump_host=None,
|
||||
ignore_ssh_config=False,
|
||||
)
|
||||
|
||||
assert temp_dir is not None
|
||||
assert (source_dir / "nginx.md").is_file()
|
||||
assert not (source_dir / "ssh.md").exists()
|
||||
|
||||
|
||||
def test_materialize_runbook_add_path_http_requires_md_suffix() -> None:
|
||||
with pytest.raises(ValueError, match="must point to a .md file"):
|
||||
_materialize_runbook_add_path(
|
||||
"https://kb.example/runbooks/",
|
||||
identity_file=None,
|
||||
jump_host=None,
|
||||
ignore_ssh_config=False,
|
||||
)
|
||||
|
||||
|
||||
def test_runbooks_sync_accepts_ssh_source(monkeypatch, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
||||
runbooks_dir = tmp_path / "remote-runbooks"
|
||||
runbooks_dir.mkdir(parents=True)
|
||||
(runbooks_dir / "nginx.md").write_text("---\nservice: nginx\n---\nbody", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tai.cli._materialize_runbooks_sync_path",
|
||||
lambda *_args, **_kwargs: (runbooks_dir, "ssh://ops@host/runbooks", None),
|
||||
)
|
||||
|
||||
class FakeStore:
|
||||
def __init__(self, _path: str, **_kwargs) -> None:
|
||||
pass
|
||||
|
||||
def sync(self, _dir: Path, _ai):
|
||||
return 1
|
||||
|
||||
monkeypatch.setattr("tai.cli.RunbookStore", FakeStore)
|
||||
monkeypatch.setattr("tai.cli.AIClient", lambda *_a, **_k: object())
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"runbooks",
|
||||
"sync",
|
||||
"--path",
|
||||
"ssh://ops@host/runbooks",
|
||||
"--store",
|
||||
"~/.tai/runbooks",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Synced 1 runbook(s)" in result.stdout
|
||||
assert "ssh://ops@host/runbooks" in result.stdout
|
||||
|
||||
|
||||
def test_runbooks_add_accepts_https_source(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
import tempfile
|
||||
|
||||
fd, temp_name = tempfile.mkstemp(prefix="tai-runbook-test-", suffix=".md")
|
||||
os.close(fd)
|
||||
Path(temp_name).write_text("---\nservice: nginx\n---\nbody", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tai.cli._materialize_runbook_add_path",
|
||||
lambda *_args, **_kwargs: (Path(temp_name), "https://kb.example/nginx.md", None),
|
||||
)
|
||||
|
||||
class FakeStore:
|
||||
def __init__(self, _path: str, **_kwargs) -> None:
|
||||
pass
|
||||
|
||||
def sync_single(self, _path: Path, _ai):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("tai.cli.RunbookStore", FakeStore)
|
||||
monkeypatch.setattr("tai.cli.AIClient", lambda *_a, **_k: object())
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"runbooks",
|
||||
"add",
|
||||
"https://kb.example/nginx.md",
|
||||
"--store",
|
||||
"~/.tai/runbooks",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Indexed" in result.stdout
|
||||
assert "https://kb.example/nginx.md" in result.stdout
|
||||
|
||||
Path(temp_name).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_inject_url_credentials_postgres() -> None:
|
||||
target = "postgresql://db.example.com:5432/tai"
|
||||
rendered = _inject_url_credentials(
|
||||
target,
|
||||
user="tai_user",
|
||||
password="secret",
|
||||
schemes={"postgresql", "postgres"},
|
||||
)
|
||||
assert rendered.startswith("postgresql://tai_user:secret@db.example.com:5432/tai")
|
||||
|
||||
|
||||
def test_inject_url_credentials_ignores_non_matching_scheme() -> None:
|
||||
target = "~/.tai/history.db"
|
||||
rendered = _inject_url_credentials(
|
||||
target,
|
||||
user="tai_user",
|
||||
password="secret",
|
||||
schemes={"postgresql", "postgres"},
|
||||
)
|
||||
assert rendered == target
|
||||
|
||||
|
||||
def test_load_env_file_and_resolve_secret(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text(
|
||||
"TAI_HISTORY_DB_USER=from_file\n"
|
||||
"TAI_HISTORY_DB_PASSWORD=from_file_pw\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
values = _load_env_file(str(env_file))
|
||||
assert values["TAI_HISTORY_DB_USER"] == "from_file"
|
||||
assert values["TAI_HISTORY_DB_PASSWORD"] == "from_file_pw"
|
||||
|
||||
monkeypatch.setenv("TAI_HISTORY_DB_USER", "from_env")
|
||||
assert _resolve_secret(None, "TAI_HISTORY_DB_USER", values) == "from_file"
|
||||
assert _resolve_secret("from_cli", "TAI_HISTORY_DB_USER", values) == "from_cli"
|
||||
|
||||
|
||||
def test_man_page_covers_cli_long_options() -> None:
|
||||
runner = CliRunner()
|
||||
help_invocations = [
|
||||
["run", "--help"],
|
||||
["history", "--help"],
|
||||
["runbooks", "sync", "--help"],
|
||||
["runbooks", "list", "--help"],
|
||||
["runbooks", "add", "--help"],
|
||||
]
|
||||
|
||||
documented = Path("docs/tai.1").read_text(encoding="utf-8")
|
||||
discovered: set[str] = set()
|
||||
for args in help_invocations:
|
||||
result = runner.invoke(app, args)
|
||||
assert result.exit_code == 0, f"help command failed for: {' '.join(args)}"
|
||||
discovered.update(re.findall(r"--[a-z0-9][a-z0-9-]*", result.stdout))
|
||||
|
||||
discovered.discard("--help")
|
||||
missing = sorted(option for option in discovered if option not in documented)
|
||||
assert missing == [], f"Missing options in docs/tai.1: {', '.join(missing)}"
|
||||
|
||||
115
tests/test_history_store.py
Normal file
115
tests/test_history_store.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Tests for SQLite-backed run history storage."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from tai.history_store import RunHistoryStore
|
||||
|
||||
|
||||
def test_history_store_add_and_count(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
store = RunHistoryStore(tmp_path / "history.db")
|
||||
assert store.count() == 0
|
||||
|
||||
payload = {
|
||||
"schema": "tai.analysis.v1",
|
||||
"generated_at": "2026-05-11T12:00:00+00:00",
|
||||
"issue": "sshd failed",
|
||||
"host": "ssh.archflux.net",
|
||||
"model": "gemma3:4b",
|
||||
"collection": {"total": 5, "failed": 1, "succeeded": 4},
|
||||
"token_usage": {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30},
|
||||
"analysis": "Root Cause...",
|
||||
}
|
||||
store.add_payload(payload)
|
||||
|
||||
assert store.count() == 1
|
||||
assert store.count(host="ssh.archflux.net") == 1
|
||||
assert store.count(host="other") == 0
|
||||
|
||||
|
||||
def test_history_store_list_host_sessions(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
store = RunHistoryStore(tmp_path / "history.db")
|
||||
store.add_payload(
|
||||
{
|
||||
"schema": "tai.analysis.v1",
|
||||
"generated_at": "2026-05-11T12:00:00+00:00",
|
||||
"issue": "issue one",
|
||||
"host": "ssh.archflux.net",
|
||||
"model": "gemma3:4b",
|
||||
"collection": {"total": 1, "failed": 0, "succeeded": 1},
|
||||
"token_usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3},
|
||||
"analysis": "first",
|
||||
}
|
||||
)
|
||||
store.add_payload(
|
||||
{
|
||||
"schema": "tai.analysis.v1",
|
||||
"generated_at": "2026-05-11T12:05:00+00:00",
|
||||
"issue": "issue two",
|
||||
"host": "ssh.archflux.net",
|
||||
"model": "gemma3:4b",
|
||||
"collection": {"total": 1, "failed": 0, "succeeded": 1},
|
||||
"token_usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3},
|
||||
"analysis": "second",
|
||||
}
|
||||
)
|
||||
|
||||
sessions = store.list_host_sessions("ssh.archflux.net", limit=2)
|
||||
assert len(sessions) == 2
|
||||
assert sessions[0].issue == "issue two"
|
||||
assert sessions[1].issue == "issue one"
|
||||
|
||||
|
||||
def test_history_store_list_recent_and_search_keyword(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
store = RunHistoryStore(tmp_path / "history.db")
|
||||
store.add_payload(
|
||||
{
|
||||
"schema": "tai.analysis.v1",
|
||||
"generated_at": "2026-05-11T13:00:00+00:00",
|
||||
"issue": "nginx failed",
|
||||
"host": "web01",
|
||||
"model": "gemma3:4b",
|
||||
"collection": {"total": 1, "failed": 0, "succeeded": 1},
|
||||
"token_usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3},
|
||||
"analysis": "nginx config typo",
|
||||
}
|
||||
)
|
||||
store.add_payload(
|
||||
{
|
||||
"schema": "tai.analysis.v1",
|
||||
"generated_at": "2026-05-11T13:10:00+00:00",
|
||||
"issue": "sshd failed",
|
||||
"host": "ssh.archflux.net",
|
||||
"model": "gemma3:4b",
|
||||
"collection": {"total": 1, "failed": 0, "succeeded": 1},
|
||||
"token_usage": {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3},
|
||||
"analysis": "sshd key mismatch",
|
||||
}
|
||||
)
|
||||
|
||||
recent = store.list_recent(limit=2)
|
||||
assert len(recent) == 2
|
||||
assert recent[0].issue == "sshd failed"
|
||||
|
||||
matches = store.search_keyword("key", host="ssh.archflux.net", limit=5)
|
||||
assert len(matches) == 1
|
||||
assert matches[0].host == "ssh.archflux.net"
|
||||
|
||||
|
||||
def test_history_store_accepts_sqlite_url(tmp_path: Path) -> None:
|
||||
db_file = tmp_path / "history-url.db"
|
||||
store = RunHistoryStore(f"sqlite:///{db_file}")
|
||||
store.add_payload(
|
||||
{
|
||||
"schema": "tai.analysis.v1",
|
||||
"generated_at": "2026-05-11T13:20:00+00:00",
|
||||
"issue": "test",
|
||||
"host": "host1",
|
||||
"model": "gemma3:4b",
|
||||
"collection": {"total": 1, "failed": 0, "succeeded": 1},
|
||||
"token_usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
||||
"analysis": "ok",
|
||||
}
|
||||
)
|
||||
assert store.count(host="host1") == 1
|
||||
@@ -100,6 +100,7 @@ def _make_chromadb_mock() -> MagicMock:
|
||||
client.get_or_create_collection.return_value = collection
|
||||
chroma_mod = MagicMock()
|
||||
chroma_mod.PersistentClient.return_value = client
|
||||
chroma_mod.HttpClient.return_value = client
|
||||
return chroma_mod
|
||||
|
||||
|
||||
@@ -251,3 +252,28 @@ def test_runbook_store_sync_single_missing_file_raises(tmp_path: Path) -> None:
|
||||
store = RunbookStore(tmp_path / "store")
|
||||
with pytest.raises(FileNotFoundError):
|
||||
store.sync_single(tmp_path / "missing.md", ai)
|
||||
|
||||
|
||||
def test_runbook_store_remote_url_uses_http_client() -> None:
|
||||
chroma_mock = _make_chromadb_mock()
|
||||
|
||||
with patch.dict("sys.modules", {"chromadb": chroma_mock}):
|
||||
store = RunbookStore("https://chroma.example.com:8443")
|
||||
assert store.count() == 0
|
||||
|
||||
chroma_mock.HttpClient.assert_called_once_with(host="chroma.example.com", port=8443, ssl=True)
|
||||
|
||||
|
||||
def test_runbook_store_remote_url_uses_http_client_with_basic_auth() -> None:
|
||||
chroma_mock = _make_chromadb_mock()
|
||||
|
||||
with patch.dict("sys.modules", {"chromadb": chroma_mock}):
|
||||
store = RunbookStore("https://chroma.example.com:8443", username="tai", password="secret")
|
||||
assert store.count() == 0
|
||||
|
||||
args = chroma_mock.HttpClient.call_args.kwargs
|
||||
assert args["host"] == "chroma.example.com"
|
||||
assert args["port"] == 8443
|
||||
assert args["ssl"] is True
|
||||
assert "headers" in args
|
||||
assert str(args["headers"].get("Authorization", "")).startswith("Basic ")
|
||||
|
||||
Reference in New Issue
Block a user