chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:12:33 +08:00
commit aacb60a4af
3387 changed files with 981307 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
"""CLI tests for `opensquilla agents`."""
from __future__ import annotations
import json
from pathlib import Path
from typer.testing import CliRunner
from opensquilla.cli.main import app
from opensquilla.onboarding.config_store import load_config
runner = CliRunner()
def _setenv(monkeypatch, tmp_path: Path) -> Path:
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
return target
def test_agents_list_json_includes_builtin_main(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(app, ["agents", "list", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload[0]["id"] == "main"
assert payload[0]["isBuiltin"] is True
def test_agents_add_json_persists_config_entry(tmp_path, monkeypatch):
target = _setenv(monkeypatch, tmp_path)
result = runner.invoke(app, ["agents", "add", "ops", "--model", "openai/test", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["id"] == "ops"
assert payload["model"] == "openai/test"
cfg = load_config(target)
assert [entry.id for entry in cfg.agents] == ["ops"]
text = target.read_text(encoding="utf-8")
assert "agents = [" in text
assert 'id = "ops"' in text
def test_agents_add_duplicate_fails(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
first = runner.invoke(app, ["agents", "add", "ops", "--json"])
assert first.exit_code == 0, first.stdout
result = runner.invoke(app, ["agents", "add", "ops", "--json"])
assert result.exit_code == 2
combined = result.stdout + (result.stderr or "")
assert "already exists" in combined
def test_agents_delete_main_rejected(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(app, ["agents", "delete", "main", "--force", "--json"])
assert result.exit_code == 2
combined = result.stdout + (result.stderr or "")
assert "builtin agent" in combined
def test_agents_delete_force_json_removes_config_entry(tmp_path, monkeypatch):
target = _setenv(monkeypatch, tmp_path)
add_result = runner.invoke(app, ["agents", "add", "ops", "--json"])
assert add_result.exit_code == 0, add_result.stdout
result = runner.invoke(app, ["agents", "delete", "ops", "--force", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload == {
"id": "ops",
"deleted": True,
"workspaceDeleted": False,
"stateDeleted": False,
}
assert load_config(target).agents == []
+88
View File
@@ -0,0 +1,88 @@
"""``opensquilla bundle`` — offline diagnostics bundle collection.
Offline: state/log dirs point at tmp_path; no gateway needs to run (live
enrichment is best-effort and silently skipped, and stubbed out here for
determinism). An autouse fixture also pins OPENSQUILLA_GATEWAY_CONFIG_PATH
to a synthetic TOML so no test can ever read or rewrite a real config.
"""
from __future__ import annotations
import json
import zipfile
from pathlib import Path
import pytest
from typer.testing import CliRunner
from opensquilla.cli.main import app
runner = CliRunner()
@pytest.fixture(autouse=True)
def _hermetic_offline(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Pin config resolution to a synthetic file and stub live enrichment.
Without the config pin, resolve_config_path(None) falls back to
./opensquilla.toml and then the developer's real home config — which the
bundle's doctor collector must never see. The enrichment stub keeps tests
deterministic and fast instead of relying on a quick connection failure.
"""
config_path = tmp_path / "synthetic-config.toml"
config_path.write_text("# synthetic bundle-cmd test config\n", encoding="utf-8")
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(config_path))
try:
from opensquilla.cli import bundle_cmd
except ImportError:
# First TDD run: module does not exist yet; let the CLI invocation
# fail with "No such command" instead of a fixture error.
return
monkeypatch.setattr(bundle_cmd, "_live_enrichment", lambda: {})
def test_bundle_writes_zip_and_prints_path(tmp_path, monkeypatch) -> None:
home = tmp_path / "home"
(home / "logs").mkdir(parents=True)
(home / "logs" / "debug.log").write_text("2026-07-07 [INFO] opensquilla: ok\n")
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
monkeypatch.setenv("OPENSQUILLA_LOG_DIR", str(home / "logs"))
output = tmp_path / "out.zip"
result = runner.invoke(app, ["bundle", "--output", str(output)])
assert result.exit_code == 0, result.output
assert output.exists()
assert str(output) in result.stdout
with zipfile.ZipFile(output) as archive:
assert "manifest.json" in archive.namelist()
def test_bundle_json_output(tmp_path, monkeypatch) -> None:
home = tmp_path / "home"
(home / "logs").mkdir(parents=True)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
monkeypatch.setenv("OPENSQUILLA_LOG_DIR", str(home / "logs"))
output = tmp_path / "out.zip"
result = runner.invoke(app, ["bundle", "--output", str(output), "--json"])
assert result.exit_code == 0, result.output
payload = json.loads(result.stdout)
assert payload["path"].endswith("out.zip")
assert payload["bundle_schema"] == 1
def test_bundle_include_content_flag(tmp_path, monkeypatch) -> None:
home = tmp_path / "home"
(home / "logs").mkdir(parents=True)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
monkeypatch.setenv("OPENSQUILLA_LOG_DIR", str(home / "logs"))
output = tmp_path / "out.zip"
result = runner.invoke(app, ["bundle", "--output", str(output), "--include-content"])
assert result.exit_code == 0, result.output
with zipfile.ZipFile(output) as archive:
manifest = json.loads(archive.read("manifest.json"))
assert manifest["content_tier"] is True
+310
View File
@@ -0,0 +1,310 @@
"""CLI tests for `opensquilla channels`."""
from __future__ import annotations
from pathlib import Path
from typer.testing import CliRunner
from opensquilla.cli.main import app
runner = CliRunner()
def _setenv(monkeypatch, tmp_path: Path) -> Path:
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
return target
def test_channels_list_empty(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(app, ["channels", "list"])
assert result.exit_code == 0
assert "0 channels" in result.stdout.lower()
def test_channels_add_telegram_polling_minimal(tmp_path, monkeypatch):
target = _setenv(monkeypatch, tmp_path)
result = runner.invoke(
app,
["channels", "add", "telegram", "--name", "tg", "--token", "abc"],
)
assert result.exit_code == 0, result.stdout
text = target.read_text()
assert "tg" in text
assert "telegram" in text
assert "abc" not in result.stdout
assert "restart" in result.stdout.lower()
def test_channels_add_slack_missing_token_fails(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(app, ["channels", "add", "slack", "--name", "w"])
assert result.exit_code != 0
combined = (result.stdout + (result.stderr or "")).lower()
assert "token" in combined
def test_channels_add_slack_succeeds_with_token(tmp_path, monkeypatch):
target = _setenv(monkeypatch, tmp_path)
result = runner.invoke(
app,
[
"channels", "add", "slack",
"--name", "w", "--token", "xoxb-x",
"--field", "signing_secret=ss",
"--field", "slack_channel_id=C123",
],
)
assert result.exit_code == 0, result.stdout
text = target.read_text()
assert "C123" in text
assert "xoxb-x" not in result.stdout
def test_channels_remove(tmp_path, monkeypatch):
target = _setenv(monkeypatch, tmp_path)
runner.invoke(app, ["channels", "add", "slack",
"--name", "w", "--token", "x",
"--field", "signing_secret=ss"])
result = runner.invoke(app, ["channels", "remove", "w"])
assert result.exit_code == 0
# Either the channel is gone, or the [[channels.channels]] table is empty.
text = target.read_text()
assert 'name = "w"' not in text
def test_channels_disable_then_enable(tmp_path, monkeypatch):
target = _setenv(monkeypatch, tmp_path)
runner.invoke(app, ["channels", "add", "slack",
"--name", "w", "--token", "x",
"--field", "signing_secret=ss"])
r1 = runner.invoke(app, ["channels", "disable", "w"])
assert r1.exit_code == 0
assert "enabled = false" in target.read_text()
r2 = runner.invoke(app, ["channels", "enable", "w"])
assert r2.exit_code == 0
assert "enabled = true" in target.read_text()
def test_channels_list_redacts_secrets(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
runner.invoke(app, ["channels", "add", "slack",
"--name", "w", "--token", "supersecret",
"--field", "signing_secret=ss"])
result = runner.invoke(app, ["channels", "list"])
assert "supersecret" not in result.stdout
assert "***" in result.stdout
def test_channels_edit_updates_only_provided_fields(tmp_path, monkeypatch):
target = _setenv(monkeypatch, tmp_path)
runner.invoke(
app,
["channels", "add", "slack", "--name", "w",
"--token", "xoxb-original",
"--field", "signing_secret=ss",
"--field", "slack_channel_id=C111"],
)
result = runner.invoke(
app,
["channels", "edit", "w", "--field", "slack_channel_id=C222"],
)
assert result.exit_code == 0, result.stdout
text = target.read_text()
assert "C222" in text
assert "C111" not in text
assert "xoxb-original" in text
def test_channels_edit_unknown_name_fails(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(app, ["channels", "edit", "missing"])
assert result.exit_code != 0
def test_channels_edit_preserves_enabled_when_unchanged(tmp_path, monkeypatch):
target = _setenv(monkeypatch, tmp_path)
runner.invoke(
app,
["channels", "add", "slack", "--name", "w",
"--token", "xoxb-x", "--field", "signing_secret=ss", "--disabled"],
)
assert "enabled = false" in target.read_text()
r = runner.invoke(
app, ["channels", "edit", "w", "--field", "slack_channel_id=C9"]
)
assert r.exit_code == 0, r.stdout
text = target.read_text()
assert "enabled = false" in text
assert "C9" in text
def test_channels_edit_preserves_agent_id_when_unchanged(tmp_path, monkeypatch):
target = _setenv(monkeypatch, tmp_path)
runner.invoke(
app,
["channels", "add", "slack", "--name", "w",
"--token", "xoxb-x", "--field", "signing_secret=ss", "--agent-id", "ops"],
)
assert 'agent_id = "ops"' in target.read_text()
r = runner.invoke(
app, ["channels", "edit", "w", "--field", "slack_channel_id=C9"]
)
assert r.exit_code == 0, r.stdout
assert 'agent_id = "ops"' in target.read_text()
def test_channels_edit_preserves_non_secret_bool_when_unchanged(
tmp_path, monkeypatch
):
target = _setenv(monkeypatch, tmp_path)
runner.invoke(
app,
["channels", "add", "slack", "--name", "w",
"--token", "xoxb-x",
"--field", "signing_secret=ss",
"--field", "reply_in_thread=true"],
)
assert "reply_in_thread = true" in target.read_text()
r = runner.invoke(
app, ["channels", "edit", "w", "--field", "slack_channel_id=C9"]
)
assert r.exit_code == 0, r.stdout
assert "reply_in_thread = true" in target.read_text()
def test_channels_add_token_resolves_to_alias_order_not_field_order(
tmp_path, monkeypatch
):
"""For wecom, --token should resolve to the literal 'token' field
(alias index 0), not 'corp_secret' (alias index 5) which would happen
under naive spec-field-order resolution.
"""
_setenv(monkeypatch, tmp_path)
result = runner.invoke(
app,
["channels", "add", "wecom",
"--name", "wc",
"--token", "wecom-token",
"--field", "corp_id=cid",
"--field", "corp_secret=cs",
"--field", "agent_id_int=1000",
"--field", "encoding_aes_key=" + "a" * 43],
)
assert result.exit_code == 0, result.stdout
assert "wecom.token" in result.stdout
assert "wecom.corp_secret" not in result.stdout
def test_channels_add_token_flag_echoes_resolved_field(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(
app, ["channels", "add", "matrix",
"--name", "m",
"--token", "syt-abc",
"--field", "homeserver_url=https://matrix.org",
"--field", "user_id=@me:matrix.org"]
)
assert result.exit_code == 0, result.stdout
assert "--token" in result.stdout
assert "matrix.access_token" in result.stdout
def test_channels_add_token_flag_for_slack_resolves_to_token(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(
app,
[
"channels", "add", "slack",
"--name", "w", "--token", "xoxb-x",
"--field", "signing_secret=ss",
],
)
assert result.exit_code == 0, result.stdout
assert "slack.token" in result.stdout
def test_channels_describe_slack(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(app, ["channels", "describe", "slack"])
assert result.exit_code == 0
out = result.stdout
assert "token" in out
assert "signing_secret" in out
assert "webhook" in out.lower()
assert "api.slack.com" in out
def test_channels_describe_unknown_type_fails(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(app, ["channels", "describe", "no-such-type"])
assert result.exit_code != 0
def test_channels_types_lists_all_supported(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(app, ["channels", "types"])
assert result.exit_code == 0
out = result.stdout
for t in ("slack", "telegram", "discord", "feishu",
"dingtalk", "wecom", "qq", "matrix"):
assert t in out
# msteams is intentionally hidden from the channel catalog CLI surface
# until first-class support lands.
assert "msteams" not in out
assert "transport" in out.lower()
def test_channels_add_restart_notice_disambiguates(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(
app,
[
"channels", "add", "slack",
"--name", "w", "--token", "x",
"--field", "signing_secret=ss",
],
)
assert result.exit_code == 0
out = result.stdout.lower()
assert "gateway process" in out
assert "not the same as" in out
assert "channels restart" in out
def test_channels_add_prints_status_verification_next_step(tmp_path, monkeypatch):
_setenv(monkeypatch, tmp_path)
result = runner.invoke(
app,
[
"channels", "add", "slack",
"--name", "w", "--token", "x",
"--field", "signing_secret=ss",
],
)
assert result.exit_code == 0
out = result.stdout.lower()
assert "opensquilla gateway restart" in out
assert "opensquilla channels status w --json" in out
def test_channels_add_echoes_resolved_path_and_source(tmp_path, monkeypatch):
monkeypatch.delenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", raising=False)
monkeypatch.setenv("HOME", str(tmp_path / "home"))
project = tmp_path / "project"
project.mkdir()
(project / "opensquilla.toml").write_text("")
monkeypatch.chdir(project)
result = runner.invoke(
app,
[
"channels", "add", "slack",
"--name", "w", "--token", "x",
"--field", "signing_secret=ss",
],
)
assert result.exit_code == 0, result.stdout
assert str(project / "opensquilla.toml") in result.stdout
assert "cwd" in result.stdout.lower()
File diff suppressed because it is too large Load Diff
+325
View File
@@ -0,0 +1,325 @@
"""Tests for the CLI ``/file`` command.
The ``/file`` command:
- Accepts the broader allow-list (PDF, text-family, JSON, plus images).
- Routes by size: <= 2 MB inlines as base64 (same shape as /image today).
- > 2 MB uploads to /api/v1/files/upload via the gateway bridge and
references the returned ``file_uuid`` in the chat.send payload.
- Hard-fails with a clear, actionable error when the bridge is
unreachable AND the file exceeds the inline cap.
These tests target the helper directly so the contract is locked
without requiring a live gateway. The hooks the helper accepts (an
``upload_callable`` parameter) make it trivial to inject a fake
bridge in tests.
"""
from __future__ import annotations
import base64
from pathlib import Path
from typing import Any
import pytest
from opensquilla.cli import chat_cmd
from opensquilla.cli.attachments import (
CLI_IMAGE_ATTACHMENT_BYTES,
CLI_INLINE_THRESHOLD_BYTES,
CLI_STAGED_PDF_BYTES,
CLI_TEXT_ATTACHMENT_BYTES,
)
from opensquilla.cli.chat_cmd import (
_file_prompt_and_attachments,
_image_prompt_and_attachments,
_image_prompt_from_command,
)
from opensquilla.cli.repl import input_bridge
def _write(tmp_path: Path, name: str, payload: bytes) -> Path:
path = tmp_path / name
path.write_bytes(payload)
return path
# ---------------------------------------------------------------------------
# Test 1 — small CSV inlines as base64.
# ---------------------------------------------------------------------------
def test_file_command_inline_for_small_csv(tmp_path: Path) -> None:
csv_bytes = b"col_a,col_b\n1,2\n3,4\n"
path = _write(tmp_path, "data.csv", csv_bytes)
prompt, attachments = _file_prompt_and_attachments(
f"/file {path} summarise this", upload_callable=None
)
assert prompt == "summarise this"
assert len(attachments) == 1
att = attachments[0]
assert att["type"] == "text/csv"
assert att["name"] == "data.csv"
assert "data" in att and "file_uuid" not in att
assert base64.b64decode(att["data"]) == csv_bytes
def test_file_command_parses_quoted_path_with_spaces(tmp_path: Path) -> None:
csv_bytes = b"col_a,col_b\n1,2\n"
path = _write(tmp_path, "data set.csv", csv_bytes)
prompt, attachments = _file_prompt_and_attachments(
f'/file "{path}" summarise this', upload_callable=None
)
assert prompt == "summarise this"
assert attachments[0]["name"] == "data set.csv"
assert attachments[0]["type"] == "text/csv"
assert base64.b64decode(attachments[0]["data"]) == csv_bytes
def test_file_command_rejects_unclosed_quoted_path() -> None:
with pytest.raises(ValueError, match=r"Usage: /file"):
_file_prompt_and_attachments('/file "unterminated', upload_callable=None)
def test_image_command_parses_quoted_path_with_spaces(tmp_path: Path) -> None:
png_bytes = b"\x89PNG\r\n\x1a\n" + b"payload"
path = _write(tmp_path, "screen shot.png", png_bytes)
prompt, attachments = _image_prompt_and_attachments(f'/image "{path}" describe it')
assert _image_prompt_from_command(f'/image "{path}" describe it') == "describe it"
assert prompt == "describe it"
assert attachments[0]["name"] == "screen shot.png"
assert attachments[0]["type"] == "image/png"
assert base64.b64decode(attachments[0]["data"]) == png_bytes
def test_image_command_reports_attachment_size(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
png_bytes = b"\x89PNG\r\n\x1a\n" + b"payload"
path = _write(tmp_path, "screen shot.png", png_bytes)
prints: list[str] = []
class FakeConsole:
def print(self, message: str) -> None:
prints.append(message)
monkeypatch.setattr(input_bridge, "console", FakeConsole())
chat_cmd._image_prompt_and_attachments(f'/image "{path}" describe it')
assert prints == ["[dim]Sending image: screen shot.png (0KB base64)[/dim]"]
# ---------------------------------------------------------------------------
# Test 2 — large PDF goes through the bridge upload callable.
# ---------------------------------------------------------------------------
def test_file_command_uses_bridge_for_large_pdf(tmp_path: Path) -> None:
big_pdf = b"%PDF-1.4\n" + b"a" * (3 * 1024 * 1024) # 3 MB > 2 MB threshold
path = _write(tmp_path, "big.pdf", big_pdf)
captured: dict[str, Any] = {}
def fake_upload(local_path: Path, mime: str, name: str) -> str:
captured["local_path"] = Path(local_path)
captured["mime"] = mime
captured["name"] = name
return "u-fake-uuid-1234"
prompt, attachments = _file_prompt_and_attachments(
f"/file {path}", upload_callable=fake_upload
)
assert prompt # default prompt assigned by the helper
assert len(attachments) == 1
att = attachments[0]
assert att["type"] == "application/pdf"
assert att["name"] == "big.pdf"
assert att["file_uuid"] == "u-fake-uuid-1234"
assert "data" not in att
assert captured["local_path"] == path
assert captured["mime"] == "application/pdf"
# ---------------------------------------------------------------------------
# Test 3 — bridge unreachable AND file > inline cap → hard-fail.
# ---------------------------------------------------------------------------
def test_file_command_hard_fails_when_bridge_unreachable_and_file_too_large(
tmp_path: Path,
) -> None:
big_pdf = b"%PDF-1.4\n" + b"a" * (3 * 1024 * 1024)
path = _write(tmp_path, "big.pdf", big_pdf)
def unreachable(local_path: Path, mime: str, name: str) -> str:
raise ConnectionError("gateway upload endpoint unavailable: connection refused")
with pytest.raises(ValueError, match=r"too large|gateway upload"):
_file_prompt_and_attachments(f"/file {path}", upload_callable=unreachable)
def test_file_command_unreachable_bridge_falls_back_to_inline_for_small_file(
tmp_path: Path,
) -> None:
"""When the bridge is unreachable but the file is below the inline cap,
the helper still inlines the bytes — never silently truncates, never fails.
"""
small_csv = b"a,b\n1,2\n"
path = _write(tmp_path, "small.csv", small_csv)
def unreachable(local_path: Path, mime: str, name: str) -> str:
raise ConnectionError("would not be invoked for inline size")
prompt, attachments = _file_prompt_and_attachments(
f"/file {path} read", upload_callable=unreachable
)
assert prompt == "read"
assert attachments[0]["type"] == "text/csv"
assert "data" in attachments[0]
def test_file_command_accepts_unknown_binary_as_opaque(tmp_path: Path) -> None:
# Unknown extension + binary content attaches as an opaque item: the bytes
# land in the agent workspace, never in the provider prompt.
path = _write(tmp_path, "x.bin", b"\x00\x01\x02\x03 binary blob")
prompt, attachments = _file_prompt_and_attachments(
f"/file {path}", upload_callable=None
)
assert prompt == "Read this file"
att = attachments[0]
assert att["type"] == "application/octet-stream"
assert base64.b64decode(att["data"]) == b"\x00\x01\x02\x03 binary blob"
def test_file_command_stages_zip_archive(tmp_path: Path) -> None:
import io
import zipfile
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w") as archive:
archive.writestr("paper/main.tex", "x" * (CLI_INLINE_THRESHOLD_BYTES + 64))
payload = buffer.getvalue()
assert len(payload) > CLI_INLINE_THRESHOLD_BYTES
path = _write(tmp_path, "paper.zip", payload)
captured: dict[str, Any] = {}
def fake_upload(local_path: Path, mime: str, name: str) -> str:
captured.update({"mime": mime, "name": name})
return "u-zip"
_prompt, attachments = _file_prompt_and_attachments(
f"/file {path}", upload_callable=fake_upload
)
assert captured["mime"] == "application/zip"
assert attachments == [
{
"type": "application/zip",
"file_uuid": "u-zip",
"name": "paper.zip",
"mime": "application/zip",
}
]
def test_file_command_accepts_unknown_utf8_text_as_plain(tmp_path: Path) -> None:
# Unknown extension whose bytes are clean UTF-8 text is accepted as
# text/plain, so the gateway's unknown-text fallback is reachable from the
# CLI rather than rejected at the client.
body = b"#!/bin/sh\necho hi\n"
path = _write(tmp_path, "script.sh", body)
prompt, attachments = _file_prompt_and_attachments(
f"/file {path} run this", upload_callable=None
)
assert prompt == "run this"
assert len(attachments) == 1
att = attachments[0]
assert att["type"] == "text/plain"
assert att["name"] == "script.sh"
assert base64.b64decode(att["data"]) == body
def test_file_command_stages_large_text_family(tmp_path: Path) -> None:
# Text above the inline threshold routes through the staged upload path
# instead of dead-ending on the old text-family rejection.
path = _write(tmp_path, "large.csv", b"a" * (CLI_TEXT_ATTACHMENT_BYTES + 1))
captured: dict[str, Any] = {}
def fake_upload(local_path: Path, mime: str, name: str) -> str:
captured.update({"local_path": local_path, "mime": mime, "name": name})
return "u-large-text"
_prompt, attachments = _file_prompt_and_attachments(
f"/file {path}", upload_callable=fake_upload
)
assert captured["mime"] == "text/csv"
assert attachments == [
{
"type": "text/csv",
"file_uuid": "u-large-text",
"name": "large.csv",
"mime": "text/csv",
}
]
def test_file_command_stages_large_image_within_image_cap(tmp_path: Path) -> None:
payload = b"\x89PNG\r\n\x1a\n" + b"a" * CLI_INLINE_THRESHOLD_BYTES
assert CLI_INLINE_THRESHOLD_BYTES < len(payload) <= CLI_IMAGE_ATTACHMENT_BYTES
path = _write(tmp_path, "large.png", payload)
captured: dict[str, Any] = {}
def fake_upload(local_path: Path, mime: str, name: str) -> str:
captured.update({"local_path": local_path, "mime": mime, "name": name})
return "u-image"
_prompt, attachments = _file_prompt_and_attachments(
f"/file {path}",
upload_callable=fake_upload,
)
assert attachments == [
{"type": "image/png", "file_uuid": "u-image", "name": "large.png", "mime": "image/png"}
]
assert captured["local_path"] == path
assert captured["mime"] == "image/png"
def test_file_command_rejects_image_above_image_cap_before_upload(tmp_path: Path) -> None:
path = _write(
tmp_path,
"too-large.png",
b"\x89PNG\r\n\x1a\n" + b"a" * CLI_IMAGE_ATTACHMENT_BYTES,
)
called = False
def fake_upload(local_path: Path, mime: str, name: str) -> str:
nonlocal called
called = True
return "u-should-not-upload"
with pytest.raises(ValueError, match=r"image attachment limit|too large"):
_file_prompt_and_attachments(f"/file {path}", upload_callable=fake_upload)
assert called is False
def test_file_command_rejects_pdf_above_staged_cap_before_upload(tmp_path: Path) -> None:
path = _write(tmp_path, "too-large.pdf", b"%PDF-1.4\n" + b"a" * CLI_STAGED_PDF_BYTES)
called = False
def fake_upload(local_path: Path, mime: str, name: str) -> str:
nonlocal called
called = True
return "u-should-not-upload"
with pytest.raises(ValueError, match=r"PDF limit|too large"):
_file_prompt_and_attachments(f"/file {path}", upload_callable=fake_upload)
assert called is False
+119
View File
@@ -0,0 +1,119 @@
from __future__ import annotations
from pathlib import Path
import pytest
from opensquilla.cli.chat_cmd import (
_parse_path_command,
_path_prompt_and_attachments,
_path_strategy_hint,
)
from opensquilla.cli.repl.commands import REGISTRY
from opensquilla.engine.commands import DEFAULT_REGISTRY, Surface
def test_path_command_parses_quoted_path_with_prompt(tmp_path: Path) -> None:
target = tmp_path / "file with spaces.log"
target.write_text("hello\n", encoding="utf-8")
prompt, attachments = _path_prompt_and_attachments(
f'/path "{target}" summarize errors'
)
assert str(target.resolve(strict=False)) in prompt
assert "summarize errors" in prompt
assert "attachments=[]" in prompt
assert attachments == []
def test_path_command_parses_unquoted_existing_path_with_spaces(tmp_path: Path) -> None:
target = tmp_path / "file with spaces.log"
target.write_text("hello\n", encoding="utf-8")
path, trailing_prompt = _parse_path_command(f"/path {target} inspect it")
assert path == target
assert trailing_prompt == "inspect it"
@pytest.mark.parametrize("unsafe", ["<", ">", "\n", "\r"])
def test_path_command_rejects_unsafe_path_token(unsafe: str) -> None:
with pytest.raises(ValueError, match="not allowed|Invalid"):
_path_prompt_and_attachments(f"/path bad{unsafe}name")
@pytest.mark.parametrize("command", ["/path", '/path "unterminated'])
def test_path_command_rejects_missing_or_unclosed_quote(command: str) -> None:
with pytest.raises(ValueError, match="Usage"):
_path_prompt_and_attachments(command)
def test_path_command_default_prompt_mentions_no_upload(tmp_path: Path) -> None:
target = tmp_path / "notes.md"
target.write_text("# Notes\n", encoding="utf-8")
prompt, attachments = _path_prompt_and_attachments(f"/path {target}")
assert "Analyze this local path" in prompt
assert "did not upload or attach file bytes" in prompt
assert "path string" in prompt
assert str(target.resolve(strict=False)) in prompt
assert attachments == []
def test_path_strategy_directory_uses_list_and_search(tmp_path: Path) -> None:
hint = _path_strategy_hint(tmp_path)
assert "list_dir" in hint
assert "glob_search" in hint
assert "grep_search" in hint
@pytest.mark.parametrize("suffix", [".csv", ".tsv", ".xlsx"])
def test_path_strategy_spreadsheet_uses_read_spreadsheet(
tmp_path: Path,
suffix: str,
) -> None:
target = tmp_path / f"data{suffix}"
target.write_bytes(b"a,b\n1,2\n")
assert "read_spreadsheet" in _path_strategy_hint(target)
@pytest.mark.parametrize("suffix", [".txt", ".log", ".md", ".json", ".html"])
def test_path_strategy_text_uses_read_file_or_grep(tmp_path: Path, suffix: str) -> None:
target = tmp_path / f"data{suffix}"
target.write_text("hello\n", encoding="utf-8")
hint = _path_strategy_hint(target)
assert "read_file" in hint
assert "grep_search" in hint
def test_path_strategy_pdf_rejects_with_file_guidance(tmp_path: Path) -> None:
target = tmp_path / "paper.pdf"
target.write_bytes(b"%PDF-1.4\n")
with pytest.raises(ValueError, match="/file"):
_path_strategy_hint(target)
@pytest.mark.parametrize("suffix", [".zip", ".exe"])
def test_path_strategy_obvious_binary_rejects(tmp_path: Path, suffix: str) -> None:
target = tmp_path / f"payload{suffix}"
target.write_bytes(b"binary")
with pytest.raises(ValueError, match="not suitable"):
_path_strategy_hint(target)
def test_path_strategy_nul_sample_rejects(tmp_path: Path) -> None:
target = tmp_path / "payload.txt"
target.write_bytes(b"abc\x00def")
with pytest.raises(ValueError, match="NUL|not suitable"):
_path_strategy_hint(target)
def test_tui_help_includes_path_command() -> None:
commands = DEFAULT_REGISTRY.for_surface(Surface.TUI)
path_command = next(cmd for cmd in commands if cmd.name == "/path")
assert path_command.usage == "/path <path> [prompt]"
assert "without uploading bytes" in path_command.description
assert any(command.usage == "/path <path> [prompt]" for command in REGISTRY)
+495
View File
@@ -0,0 +1,495 @@
"""Unit tests for CLI clarify_form pure helpers and async prompter (PR6)."""
from __future__ import annotations
import pytest
from opensquilla.cli.repl.clarify_form import (
ClarifyFormResult,
coerce_field_input,
fields_to_chat_send_message,
is_cancel_token,
prompt_clarify_form,
render_field_label,
)
# ── render_field_label ──
def test_label_string_required_with_prompt():
label = render_field_label({
"name": "destination", "type": "string", "required": True,
"prompt": "目的地",
})
assert label == "destination (string, required): 目的地"
def test_label_int_with_range():
label = render_field_label({
"name": "days", "type": "int", "required": True,
"prompt": "天数", "min": 1, "max": 14,
})
assert label == "days (int 1-14, required): 天数"
def test_label_enum_with_choices_and_default():
label = render_field_label({
"name": "budget", "type": "enum", "required": False,
"prompt": "预算", "choices": ["budget", "mid", "premium"],
"default": "mid",
})
assert label == "budget (enum [budget|mid|premium], default=mid): 预算"
def test_label_string_with_max_chars():
label = render_field_label({
"name": "notes", "type": "string", "required": False,
"prompt": "备注", "max_chars": 100,
})
assert "≤100 chars" in label
assert "optional" in label
def test_label_int_only_min():
label = render_field_label({
"name": "n", "type": "int", "required": True, "min": 5,
})
assert ">=5" in label
def test_label_int_only_max():
label = render_field_label({
"name": "n", "type": "int", "required": True, "max": 100,
})
assert "<=100" in label
def test_label_without_prompt():
label = render_field_label({"name": "x", "type": "string", "required": True})
assert label == "x (string, required)"
# ── coerce_field_input ──
def test_coerce_string_simple():
value, err = coerce_field_input(
{"name": "x", "type": "string", "required": True}, "Tokyo",
)
assert err is None
assert value == "Tokyo"
def test_coerce_string_max_chars_exceeded():
value, err = coerce_field_input(
{"name": "n", "type": "string", "required": True, "max_chars": 5},
"longer than five",
)
assert value is None
assert "max_chars" in err
def test_coerce_int_valid():
value, err = coerce_field_input(
{"name": "days", "type": "int", "required": True, "min": 1, "max": 14},
"5",
)
assert err is None
assert value == 5
def test_coerce_int_non_numeric():
value, err = coerce_field_input(
{"name": "days", "type": "int", "required": True}, "abc",
)
assert value is None
assert "integer" in err
def test_coerce_int_below_min():
value, err = coerce_field_input(
{"name": "days", "type": "int", "required": True, "min": 1, "max": 14},
"0",
)
assert value is None
assert "min=1" in err
def test_coerce_int_above_max():
value, err = coerce_field_input(
{"name": "days", "type": "int", "required": True, "min": 1, "max": 14},
"100",
)
assert value is None
assert "max=14" in err
def test_coerce_bool_true_variants():
for s in ("true", "True", "yes", "Y", "1", ""):
value, err = coerce_field_input(
{"name": "b", "type": "bool", "required": True}, s,
)
assert err is None, s
assert value is True, s
def test_coerce_bool_false_variants():
for s in ("false", "False", "no", "N", "0", ""):
value, err = coerce_field_input(
{"name": "b", "type": "bool", "required": True}, s,
)
assert err is None, s
assert value is False, s
def test_coerce_bool_invalid():
value, err = coerce_field_input(
{"name": "b", "type": "bool", "required": True}, "maybe",
)
assert value is None
assert "bool" in err
def test_coerce_enum_valid():
value, err = coerce_field_input(
{"name": "budget", "type": "enum", "required": True,
"choices": ["budget", "mid", "premium"]},
"mid",
)
assert err is None
assert value == "mid"
def test_coerce_enum_invalid():
value, err = coerce_field_input(
{"name": "budget", "type": "enum", "required": True,
"choices": ["budget", "mid", "premium"]},
"luxury",
)
assert value is None
assert "luxury" in err
def test_coerce_required_empty_errors():
value, err = coerce_field_input(
{"name": "x", "type": "string", "required": True}, "",
)
assert value is None
assert "required" in err
def test_coerce_optional_empty_is_silent():
"""Empty input on an optional field returns (None, None) so the caller
can simply skip the field — not an error condition."""
value, err = coerce_field_input(
{"name": "x", "type": "string", "required": False}, "",
)
assert value is None
assert err is None
def test_coerce_whitespace_only_treated_as_empty():
value, err = coerce_field_input(
{"name": "x", "type": "string", "required": True}, " ",
)
assert value is None
assert "required" in err
# ── is_cancel_token ──
def test_cancel_token_exact_match():
assert is_cancel_token("cancel", ("cancel", "取消"))
def test_cancel_token_case_insensitive():
assert is_cancel_token("CANCEL", ("cancel",))
def test_cancel_token_trims_whitespace():
assert is_cancel_token(" cancel ", ("cancel",))
def test_cancel_token_no_match_when_substring():
"""The token must match the WHOLE input, not be a substring of it."""
assert not is_cancel_token("I want to cancel my booking", ("cancel",))
def test_cancel_token_empty_keywords_false():
assert not is_cancel_token("anything", ())
def test_cancel_token_cjk():
assert is_cancel_token("取消", ("取消", "cancel"))
# ── fields_to_chat_send_message ──
def test_fields_to_chat_send_message_basic():
out = fields_to_chat_send_message({
"destination": "Tokyo",
"days": 5,
})
assert "destination: Tokyo" in out
assert "days: 5" in out
def test_fields_to_chat_send_message_bool_lowercase():
out = fields_to_chat_send_message({"flag": True, "off": False})
assert "flag: true" in out
assert "off: false" in out
def test_fields_to_chat_send_message_skips_empty():
out = fields_to_chat_send_message({
"destination": "Tokyo",
"notes": "",
"extra": None,
})
assert "destination: Tokyo" in out
assert "notes" not in out
assert "extra" not in out
# ── prompt_clarify_form (async, stubbed prompt) ──
@pytest.mark.asyncio
async def test_prompt_form_happy_path():
"""Walk through 2 fields with valid input; collect both."""
schema = {
"intro": "trip facts please",
"fields": [
{"name": "destination", "type": "string", "required": True,
"prompt": "目的地"},
{"name": "days", "type": "int", "required": True, "min": 1, "max": 14,
"prompt": "天数"},
],
"cancel_keywords": [],
}
answers = iter(["Tokyo", "5"])
out_lines: list[str] = []
async def _stub_prompt(prefix: str) -> str | None:
return next(answers)
result = await prompt_clarify_form(
schema, prompt_fn=_stub_prompt, writer=out_lines.append,
)
assert result.cancelled is False
assert result.fields == {"destination": "Tokyo", "days": 5}
# intro was printed
assert any("trip facts" in line for line in out_lines)
@pytest.mark.asyncio
async def test_prompt_form_retries_on_validation_error():
"""First answer fails int validation; second succeeds; field collected."""
schema = {
"fields": [
{"name": "days", "type": "int", "required": True, "min": 1, "max": 14},
],
"cancel_keywords": [],
}
answers = iter(["abc", "5"])
out_lines: list[str] = []
async def _stub(prefix: str) -> str | None:
return next(answers)
result = await prompt_clarify_form(
schema, prompt_fn=_stub, writer=out_lines.append,
)
assert result.cancelled is False
assert result.fields == {"days": 5}
# error message was printed for the bad input
assert any("integer" in line for line in out_lines)
@pytest.mark.asyncio
async def test_prompt_form_cancel_keyword_bails():
schema = {
"fields": [
{"name": "x", "type": "string", "required": True},
{"name": "y", "type": "string", "required": True},
],
"cancel_keywords": ["cancel"],
}
answers = iter(["Tokyo", "cancel"])
async def _stub(prefix: str) -> str | None:
return next(answers)
result = await prompt_clarify_form(schema, prompt_fn=_stub, writer=lambda _: None)
assert result.cancelled is True
assert result.fields == {}
@pytest.mark.asyncio
async def test_prompt_form_eof_bails():
"""Ctrl-D (prompt_fn returns None) cancels the whole form."""
schema = {
"fields": [
{"name": "x", "type": "string", "required": True},
],
"cancel_keywords": [],
}
async def _stub(prefix: str) -> str | None:
return None
result = await prompt_clarify_form(schema, prompt_fn=_stub, writer=lambda _: None)
assert result.cancelled is True
assert result.fields == {}
@pytest.mark.asyncio
async def test_prompt_form_optional_field_can_be_skipped():
schema = {
"fields": [
{"name": "destination", "type": "string", "required": True},
{"name": "notes", "type": "string", "required": False},
],
"cancel_keywords": [],
}
answers = iter(["Tokyo", ""])
async def _stub(prefix: str) -> str | None:
return next(answers)
result = await prompt_clarify_form(schema, prompt_fn=_stub, writer=lambda _: None)
assert result.cancelled is False
assert result.fields == {"destination": "Tokyo"}
def test_result_dataclass_minimal():
r = ClarifyFormResult(fields={"x": 1}, cancelled=False)
assert r.fields == {"x": 1}
assert r.cancelled is False
# ── Surface rendering of (d) protocol: confirmed / ambiguous / unknowns ──
@pytest.mark.asyncio
async def test_prompt_form_renders_confirmed_fields_and_accepts_enter() -> None:
"""When the schema carries ``confirmed_fields`` from the prefill
scan, the user must see the inferred values up-front and be able
to accept each by hitting Enter on an empty line. The accepted
value lands in the result exactly as the audit reported."""
schema = {
"mode": "form",
"intro": "",
"fields": [
{"name": "destination", "type": "string", "required": True, "prompt": "city"},
{
"name": "days",
"type": "int",
"required": True,
"prompt": "days",
"min": 1,
"max": 30,
},
],
"cancel_keywords": [],
"timeout_hours": 24,
"confirmed_fields": [
{"name": "destination", "value": "Tokyo", "source": "auto_prefill"},
],
"ambiguous_fields": [
{"name": "days", "reason": "duration not stated"},
],
"unknown_mentions": [],
}
answers = iter(["", "5"]) # Enter on destination → confirm; "5" for days
async def _stub(_prefix: str) -> str | None:
return next(answers)
out: list[str] = []
result = await prompt_clarify_form(schema, prompt_fn=_stub, writer=out.append)
assert result.cancelled is False
assert result.fields == {"destination": "Tokyo", "days": 5}
transcript = "\n".join(out)
assert "noticed details" in transcript
assert "destination" in transcript
assert "Tokyo" in transcript
assert "duration not stated" in transcript
@pytest.mark.asyncio
async def test_prompt_form_confirmed_field_can_be_overridden() -> None:
"""A user who disagrees with the inferred value must be able to
type a new value to override. The overridden value wins; the
confirmed value is discarded."""
schema = {
"mode": "form",
"intro": "",
"fields": [
{"name": "destination", "type": "string", "required": True, "prompt": "city"},
],
"cancel_keywords": [],
"timeout_hours": 24,
"confirmed_fields": [
{"name": "destination", "value": "Tokyo", "source": "auto_prefill"},
],
}
async def _stub(_prefix: str) -> str | None:
return "Osaka"
result = await prompt_clarify_form(schema, prompt_fn=_stub, writer=lambda _: None)
assert result.cancelled is False
assert result.fields == {"destination": "Osaka"}
@pytest.mark.asyncio
async def test_prompt_form_renders_unknown_mentions() -> None:
"""``unknown_mentions`` must surface verbatim so the user knows the
system noticed something it could not map to a field."""
schema = {
"mode": "form",
"intro": "",
"fields": [
{"name": "destination", "type": "string", "required": True, "prompt": "city"},
],
"cancel_keywords": [],
"timeout_hours": 24,
"confirmed_fields": [],
"ambiguous_fields": [],
"unknown_mentions": [
{"text": "next month", "guess": "departure timing?"},
],
}
async def _stub(_prefix: str) -> str | None:
return "Tokyo"
out: list[str] = []
await prompt_clarify_form(schema, prompt_fn=_stub, writer=out.append)
transcript = "\n".join(out)
assert "next month" in transcript
assert "departure timing?" in transcript
@pytest.mark.asyncio
async def test_prompt_form_without_prefill_payload_unchanged() -> None:
"""Backwards compatibility: a schema without any prefill payload
must render exactly as before — no transparency header, no extra
blocks, no behavioural change."""
schema = {
"mode": "form",
"intro": "",
"fields": [
{"name": "destination", "type": "string", "required": True, "prompt": "city"},
],
"cancel_keywords": [],
"timeout_hours": 24,
}
async def _stub(_prefix: str) -> str | None:
return "Tokyo"
out: list[str] = []
result = await prompt_clarify_form(schema, prompt_fn=_stub, writer=out.append)
assert result.fields == {"destination": "Tokyo"}
transcript = "\n".join(out)
assert "noticed details" not in transcript
File diff suppressed because it is too large Load Diff
+921
View File
@@ -0,0 +1,921 @@
"""CLI cron command coverage.
Verifies the new helpers (_parse_duration_seconds, _resolve_webhook_token,
_build_delivery_params) and that cron add/update build the right RPC params
when the new flags are exercised. Network is stubbed so we never actually
hit a gateway.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import pytest
import typer
from opensquilla.cli import cron_cmd
# --- _parse_duration_seconds ---------------------------------------------
def test_parse_duration_none_returns_none() -> None:
assert cron_cmd._parse_duration_seconds(None) is None
assert cron_cmd._parse_duration_seconds("") is None
def test_parse_duration_seconds_suffix() -> None:
assert cron_cmd._parse_duration_seconds("30s") == 30.0
assert cron_cmd._parse_duration_seconds("5m") == 300.0
assert cron_cmd._parse_duration_seconds("1h") == 3600.0
assert cron_cmd._parse_duration_seconds("90sec") == 90.0
assert cron_cmd._parse_duration_seconds("2hrs") == 7200.0
def test_parse_duration_plain_number() -> None:
assert cron_cmd._parse_duration_seconds("45") == 45.0
assert cron_cmd._parse_duration_seconds("0") == 0.0
assert cron_cmd._parse_duration_seconds(60) == 60.0
assert cron_cmd._parse_duration_seconds(60.5) == 60.5
def test_parse_duration_rejects_garbage() -> None:
with pytest.raises(typer.BadParameter):
cron_cmd._parse_duration_seconds("nope")
with pytest.raises(typer.BadParameter):
cron_cmd._parse_duration_seconds("5x")
# --- _resolve_webhook_token ----------------------------------------------
def test_webhook_token_none_when_no_source() -> None:
assert cron_cmd._resolve_webhook_token(inline=None, env=None, path=None) is None
def test_webhook_token_from_env(monkeypatch) -> None:
monkeypatch.setenv("MY_HOOK_TOKEN", "secret-bearer")
assert (
cron_cmd._resolve_webhook_token(inline=None, env="MY_HOOK_TOKEN", path=None)
== "secret-bearer"
)
def test_webhook_token_from_env_unset(monkeypatch) -> None:
monkeypatch.delenv("UNSET_TOKEN", raising=False)
with pytest.raises(typer.BadParameter, match="UNSET_TOKEN"):
cron_cmd._resolve_webhook_token(inline=None, env="UNSET_TOKEN", path=None)
def test_webhook_token_from_file(tmp_path: Path) -> None:
secret = tmp_path / "token.txt"
secret.write_text(" file-token \n", encoding="utf-8")
assert (
cron_cmd._resolve_webhook_token(inline=None, env=None, path=str(secret))
== "file-token"
)
def test_webhook_token_inline_emits_warning(capsys) -> None:
out = cron_cmd._resolve_webhook_token(
inline="raw-token", env=None, path=None
)
assert out == "raw-token"
err = capsys.readouterr().err
assert "shell history" in err
def test_webhook_token_rejects_multiple_sources(monkeypatch, tmp_path: Path) -> None:
monkeypatch.setenv("T", "x")
secret = tmp_path / "t.txt"
secret.write_text("y", encoding="utf-8")
with pytest.raises(typer.BadParameter, match="at most one"):
cron_cmd._resolve_webhook_token(inline=None, env="T", path=str(secret))
# --- _build_delivery_params ---------------------------------------------
def test_build_delivery_none_when_no_flags() -> None:
assert (
cron_cmd._build_delivery_params(
announce=False,
no_deliver=False,
channel=None,
to=None,
account=None,
best_effort=False,
webhook_url=None,
webhook_token=None,
)
is None
)
def test_build_delivery_announce_with_target() -> None:
d = cron_cmd._build_delivery_params(
announce=True,
no_deliver=False,
channel="slack",
to="C123",
account=None,
best_effort=False,
webhook_url=None,
webhook_token=None,
)
assert d == {"mode": "announce", "channelName": "slack", "to": "C123"}
def test_build_delivery_announce_with_last_omits_channel_name() -> None:
"""'last' is a CLI sentinel meaning 'let the backend infer'."""
d = cron_cmd._build_delivery_params(
announce=True,
no_deliver=False,
channel="last",
to=None,
account=None,
best_effort=False,
webhook_url=None,
webhook_token=None,
)
assert d == {"mode": "announce"}
assert "channelName" not in d
def test_build_delivery_no_deliver() -> None:
d = cron_cmd._build_delivery_params(
announce=False,
no_deliver=True,
channel=None,
to=None,
account=None,
best_effort=False,
webhook_url=None,
webhook_token=None,
)
assert d == {"mode": "none"}
def test_build_delivery_webhook() -> None:
d = cron_cmd._build_delivery_params(
announce=False,
no_deliver=False,
channel=None,
to=None,
account=None,
best_effort=True,
webhook_url="https://hooks.example/cron",
webhook_token="bearer-x",
)
assert d == {
"mode": "webhook",
"webhookUrl": "https://hooks.example/cron",
"webhookToken": "bearer-x",
"bestEffort": True,
}
def test_build_delivery_mutex_webhook_vs_announce() -> None:
with pytest.raises(typer.BadParameter, match="at most one"):
cron_cmd._build_delivery_params(
announce=True,
no_deliver=False,
channel=None,
to=None,
account=None,
best_effort=False,
webhook_url="https://hooks.example/x",
webhook_token=None,
)
def test_build_delivery_mutex_announce_vs_no_deliver() -> None:
with pytest.raises(typer.BadParameter, match="at most one"):
cron_cmd._build_delivery_params(
announce=True,
no_deliver=True,
channel=None,
to=None,
account=None,
best_effort=False,
webhook_url=None,
webhook_token=None,
)
def test_build_delivery_token_without_url() -> None:
with pytest.raises(typer.BadParameter, match="requires --webhook-url"):
cron_cmd._build_delivery_params(
announce=False,
no_deliver=False,
channel=None,
to=None,
account=None,
best_effort=False,
webhook_url=None,
webhook_token="dangling-token",
)
def test_build_delivery_with_account_implies_announce() -> None:
d = cron_cmd._build_delivery_params(
announce=False,
no_deliver=False,
channel="slack",
to="C123",
account="acct-1",
best_effort=True,
webhook_url=None,
webhook_token=None,
)
assert d == {
"mode": "announce",
"channelName": "slack",
"to": "C123",
"accountId": "acct-1",
"bestEffort": True,
}
# --- _build_failure_destination_dict -------------------------------------
def test_failure_dest_none_when_no_flags() -> None:
assert (
cron_cmd._build_failure_destination_dict(
mode=None,
channel=None,
to=None,
account=None,
webhook_url=None,
webhook_token=None,
)
is None
)
def test_failure_dest_webhook() -> None:
fd = cron_cmd._build_failure_destination_dict(
mode="webhook",
channel=None,
to=None,
account=None,
webhook_url="https://hooks.example/alert",
webhook_token="bearer-x",
)
assert fd == {
"mode": "webhook",
"webhookUrl": "https://hooks.example/alert",
"webhookToken": "bearer-x",
}
def test_failure_dest_channel() -> None:
fd = cron_cmd._build_failure_destination_dict(
mode="channel",
channel="Slack",
to="C-ops",
account="acct-1",
webhook_url=None,
webhook_token=None,
)
assert fd == {
"mode": "channel",
"channelName": "slack",
"to": "C-ops",
"accountId": "acct-1",
}
def test_failure_dest_webhook_requires_url() -> None:
with pytest.raises(typer.BadParameter, match="--failure-webhook-url"):
cron_cmd._build_failure_destination_dict(
mode="webhook",
channel=None,
to=None,
account=None,
webhook_url=None,
webhook_token=None,
)
def test_failure_dest_rejects_unknown_mode() -> None:
with pytest.raises(typer.BadParameter, match="channel.*webhook"):
cron_cmd._build_failure_destination_dict(
mode="email",
channel=None,
to=None,
account=None,
webhook_url=None,
webhook_token=None,
)
def test_failure_dest_flag_without_mode() -> None:
with pytest.raises(typer.BadParameter, match="--failure-mode"):
cron_cmd._build_failure_destination_dict(
mode=None,
channel="slack",
to="C-x",
account=None,
webhook_url=None,
webhook_token=None,
)
# --- cron_add / cron_update RPC param construction ------------------------
class _StubClient:
def __init__(self) -> None:
self.calls: list[tuple[str, dict[str, Any]]] = []
async def call(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
self.calls.append((method, params))
return {"id": "stub", "method": method}
@pytest.fixture
def stub_gateway(monkeypatch):
client = _StubClient()
def _runner(fn, *, json_output: bool = False): # noqa: ARG001
import asyncio
return asyncio.run(fn(client))
monkeypatch.setattr(cron_cmd, "run_gateway_sync", _runner)
monkeypatch.setattr(cron_cmd, "confirm_or_exit", lambda *a, **kw: None)
monkeypatch.setattr(cron_cmd, "_emit_success", lambda *a, **kw: None)
return client
def test_cron_add_with_announce_and_channel(stub_gateway) -> None:
cron_cmd.cron_add(
expression="0 9 * * *",
text="Daily brief",
name=None,
agent=None,
session_target="isolated",
timeout=None,
tz=None,
wake=None,
exact=False,
jitter=None,
announce=True,
no_deliver=False,
channel="slack",
to="C123",
account=None,
best_effort_deliver=False,
webhook_url=None,
webhook_token=None,
webhook_token_env=None,
webhook_token_file=None,
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
assert stub_gateway.calls, "no RPC call was issued"
method, params = stub_gateway.calls[-1]
assert method == "cron.add"
assert params["delivery"] == {
"mode": "announce",
"channelName": "slack",
"to": "C123",
}
def test_cron_add_rejects_current_target_without_session_binding(stub_gateway) -> None:
with pytest.raises(typer.BadParameter, match="current.*session-bound"):
cron_cmd.cron_add(
expression="0 9 * * *",
text="Daily brief",
name=None,
agent=None,
session_target="current",
timeout=None,
tz=None,
wake=None,
exact=False,
jitter=None,
announce=False,
no_deliver=False,
channel=None,
to=None,
account=None,
best_effort_deliver=False,
webhook_url=None,
webhook_token=None,
webhook_token_env=None,
webhook_token_file=None,
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
assert stub_gateway.calls == []
def test_cron_add_with_webhook(stub_gateway, monkeypatch) -> None:
monkeypatch.setenv("HOOK_TOKEN", "from-env")
cron_cmd.cron_add(
expression="*/5 * * * *",
text="poke",
name=None,
agent=None,
session_target="isolated",
timeout=None,
tz=None,
wake=None,
exact=False,
jitter=None,
announce=False,
no_deliver=False,
channel=None,
to=None,
account=None,
best_effort_deliver=False,
webhook_url="https://hooks.example/cron",
webhook_token=None,
webhook_token_env="HOOK_TOKEN",
webhook_token_file=None,
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
method, params = stub_gateway.calls[-1]
assert method == "cron.add"
assert params["delivery"] == {
"mode": "webhook",
"webhookUrl": "https://hooks.example/cron",
"webhookToken": "from-env",
}
def test_cron_add_with_failure_destination(stub_gateway) -> None:
cron_cmd.cron_add(
expression="0 9 * * *",
text="Daily brief",
name=None,
agent=None,
session_target="isolated",
timeout=None,
tz=None,
wake=None,
exact=False,
jitter=None,
announce=True,
no_deliver=False,
channel="slack",
to="C123",
account=None,
best_effort_deliver=False,
webhook_url=None,
webhook_token=None,
webhook_token_env=None,
webhook_token_file=None,
failure_mode="webhook",
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url="https://hooks.example/alert",
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
method, params = stub_gateway.calls[-1]
assert method == "cron.add"
assert params["delivery"] == {
"mode": "announce",
"channelName": "slack",
"to": "C123",
"failureDestination": {
"mode": "webhook",
"webhookUrl": "https://hooks.example/alert",
},
}
def test_cron_add_with_wake_and_jitter_duration(stub_gateway) -> None:
cron_cmd.cron_add(
expression="*/5 * * * *",
text="x",
name=None,
agent=None,
session_target="main",
timeout=None,
tz=None,
wake="next-heartbeat",
exact=False,
jitter="30s",
announce=False,
no_deliver=False,
channel=None,
to=None,
account=None,
best_effort_deliver=False,
webhook_url=None,
webhook_token=None,
webhook_token_env=None,
webhook_token_file=None,
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
method, params = stub_gateway.calls[-1]
assert params["wakeMode"] == "next-heartbeat"
assert params["jitterSeconds"] == 30.0
assert "delivery" not in params # no delivery flags → not sent
def test_cron_add_with_every_builds_canonical_schedule(stub_gateway) -> None:
cron_cmd.cron_add(
expression=None,
cron=None,
every="5m",
at=None,
text="Drink water",
name=None,
agent=None,
session_target="isolated",
timeout=None,
tz=None,
wake=None,
exact=False,
jitter=None,
announce=False,
no_deliver=False,
channel=None,
to=None,
account=None,
best_effort_deliver=False,
webhook_url=None,
webhook_token=None,
webhook_token_env=None,
webhook_token_file=None,
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
method, params = stub_gateway.calls[-1]
assert method == "cron.add"
assert params["schedule"] == {"kind": "every", "every_seconds": 300}
assert params["payloadKind"] == "reminder"
assert "expression" not in params
def test_cron_add_rejects_multiple_schedule_sources(stub_gateway) -> None:
with pytest.raises(typer.BadParameter, match="exactly one"):
cron_cmd.cron_add(
expression="*/5 * * * *",
cron="0 9 * * *",
every=None,
at=None,
text="x",
name=None,
agent=None,
session_target="isolated",
timeout=None,
tz=None,
wake=None,
exact=False,
jitter=None,
announce=False,
no_deliver=False,
channel=None,
to=None,
account=None,
best_effort_deliver=False,
webhook_url=None,
webhook_token=None,
webhook_token_env=None,
webhook_token_file=None,
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
def test_cron_add_rejects_fractional_every(stub_gateway) -> None:
with pytest.raises(typer.BadParameter, match="whole seconds"):
cron_cmd.cron_add(
expression=None,
cron=None,
every="1.9s",
at=None,
text="x",
name=None,
agent=None,
session_target="isolated",
timeout=None,
tz=None,
wake=None,
exact=False,
jitter=None,
announce=False,
no_deliver=False,
channel=None,
to=None,
account=None,
best_effort_deliver=False,
webhook_url=None,
webhook_token=None,
webhook_token_env=None,
webhook_token_file=None,
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
assert stub_gateway.calls == []
def test_cron_add_rejects_invalid_wake(stub_gateway) -> None:
with pytest.raises(typer.BadParameter, match="now or next-heartbeat"):
cron_cmd.cron_add(
expression="*/5 * * * *",
text="x",
name=None,
agent=None,
session_target="main",
timeout=None,
tz=None,
wake="LATER",
exact=False,
jitter=None,
announce=False,
no_deliver=False,
channel=None,
to=None,
account=None,
best_effort_deliver=False,
webhook_url=None,
webhook_token=None,
webhook_token_env=None,
webhook_token_file=None,
json_output=False,
)
def test_cron_update_with_wake(stub_gateway) -> None:
cron_cmd.cron_update(
job_id="job-1",
expression=None,
cron=None,
every=None,
at=None,
text=None,
name=None,
enabled=None,
timeout=None,
tz=None,
wake="now",
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
method, params = stub_gateway.calls[-1]
assert method == "cron.update"
assert params["id"] == "job-1"
assert params["wakeMode"] == "now"
def test_cron_update_with_every_builds_canonical_schedule(stub_gateway) -> None:
cron_cmd.cron_update(
job_id="job-1",
expression=None,
cron=None,
every="10m",
at=None,
text=None,
name=None,
enabled=None,
timeout=None,
tz=None,
wake=None,
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
method, params = stub_gateway.calls[-1]
assert method == "cron.update"
assert params["schedule"] == {"kind": "every", "every_seconds": 600}
assert "expression" not in params
def test_cron_update_with_tz_only_sends_timezone_patch(stub_gateway) -> None:
cron_cmd.cron_update(
job_id="job-1",
expression=None,
cron=None,
every=None,
at=None,
text=None,
name=None,
enabled=None,
timeout=None,
tz="Asia/Shanghai",
wake=None,
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
method, params = stub_gateway.calls[-1]
assert method == "cron.update"
assert params == {"id": "job-1", "tz": "Asia/Shanghai"}
def test_cron_update_rejects_multiple_schedule_sources(stub_gateway) -> None:
with pytest.raises(typer.BadParameter, match="at most one"):
cron_cmd.cron_update(
job_id="job-1",
expression="*/5 * * * *",
cron="0 9 * * *",
every=None,
at=None,
text=None,
name=None,
enabled=None,
timeout=None,
tz=None,
wake=None,
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
def test_cron_update_requires_at_least_one_field(stub_gateway) -> None:
with pytest.raises(typer.BadParameter, match="at least one"):
cron_cmd.cron_update(
job_id="job-1",
expression=None,
text=None,
name=None,
enabled=None,
timeout=None,
tz=None,
wake=None,
failure_mode=None,
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
def test_cron_update_with_failure_destination_webhook(stub_gateway) -> None:
cron_cmd.cron_update(
job_id="job-1",
expression=None,
text=None,
name=None,
enabled=None,
timeout=None,
tz=None,
wake=None,
failure_mode="webhook",
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url="https://hooks.example/alert",
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
method, params = stub_gateway.calls[-1]
assert method == "cron.update"
assert params["delivery"] == {
"failureDestination": {
"mode": "webhook",
"webhookUrl": "https://hooks.example/alert",
}
}
def test_cron_update_with_failure_destination_channel(stub_gateway) -> None:
cron_cmd.cron_update(
job_id="job-1",
expression=None,
text=None,
name=None,
enabled=None,
timeout=None,
tz=None,
wake=None,
failure_mode="channel",
failure_channel="slack",
failure_to="C-ops",
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
method, params = stub_gateway.calls[-1]
assert params["delivery"] == {
"failureDestination": {
"mode": "channel",
"channelName": "slack",
"to": "C-ops",
}
}
def test_cron_update_failure_webhook_missing_url(stub_gateway) -> None:
with pytest.raises(typer.BadParameter, match="--failure-webhook-url"):
cron_cmd.cron_update(
job_id="job-1",
expression=None,
text=None,
name=None,
enabled=None,
timeout=None,
tz=None,
wake=None,
failure_mode="webhook",
failure_channel=None,
failure_to=None,
failure_account=None,
failure_webhook_url=None,
failure_webhook_token=None,
failure_webhook_token_env=None,
failure_webhook_token_file=None,
json_output=False,
)
+73
View File
@@ -0,0 +1,73 @@
from __future__ import annotations
import json
from typing import Any
from typer.testing import CliRunner
from opensquilla.cli.main import app
runner = CliRunner()
class _FakeGatewayClient:
calls: list[tuple[str, dict[str, Any]]] = []
payloads: dict[str, Any] = {}
async def connect(self, url: str, *, token: str | None = None) -> None:
type(self).calls.append(("connect", {"url": url, "token": token}))
async def close(self) -> None:
type(self).calls.append(("close", {}))
async def call(self, method: str, params: dict | None = None) -> Any:
type(self).calls.append((method, params or {}))
return type(self).payloads.get(method, {})
def _install_fake_gateway(monkeypatch) -> type[_FakeGatewayClient]:
_FakeGatewayClient.calls = []
_FakeGatewayClient.payloads = {
"diagnostics.status": {
"enabled": False,
"detail": "off",
"raw_turn_call": {"enabled": False, "source": "off", "env_override": False},
"applies_to": "next_turn",
},
"diagnostics.set": {
"enabled": True,
"detail": "standard",
"raw_turn_call": {"enabled": False, "source": "off", "env_override": False},
"applies_to": "next_turn",
},
}
monkeypatch.setattr("opensquilla.cli.gateway_client.GatewayClient", _FakeGatewayClient)
return _FakeGatewayClient
def test_diagnostics_status_uses_status_rpc(monkeypatch) -> None:
fake = _install_fake_gateway(monkeypatch)
result = runner.invoke(app, ["diagnostics", "status", "--json"])
assert result.exit_code == 0, result.stdout
assert json.loads(result.stdout)["detail"] == "off"
assert ("diagnostics.status", {}) in fake.calls
def test_diagnostics_on_raw_uses_set_rpc(monkeypatch) -> None:
fake = _install_fake_gateway(monkeypatch)
result = runner.invoke(app, ["diagnostics", "on", "--raw", "--json"])
assert result.exit_code == 0, result.stdout
assert ("diagnostics.set", {"enabled": True, "raw": True}) in fake.calls
def test_diagnostics_off_uses_set_rpc(monkeypatch) -> None:
fake = _install_fake_gateway(monkeypatch)
result = runner.invoke(app, ["diagnostics", "off", "--json"])
assert result.exit_code == 0, result.stdout
assert ("diagnostics.set", {"enabled": False}) in fake.calls
File diff suppressed because it is too large Load Diff
+107
View File
@@ -0,0 +1,107 @@
"""CLI tests for `opensquilla ensemble bench` (offline dry-run only).
The dry-run path drives synthetic providers through a scripted FailureInjector,
so these tests never touch the network. The live path is exercised only under a
live invocation and is not covered here.
"""
from __future__ import annotations
import json
import textwrap
from pathlib import Path
from typer.testing import CliRunner
from opensquilla.cli.main import app
runner = CliRunner()
def test_bench_dry_run_json_shape() -> None:
result = runner.invoke(app, ["ensemble", "bench", "--dry-run", "--json"])
assert result.exit_code == 0, result.output
report = json.loads(result.stdout)
assert set(report) == {"ensemble", "baseline", "deltas"}
ens = report["ensemble"]
for key in (
"label",
"runs",
"successes",
"failures",
"success_rate",
"mean_latency_s",
"p95_latency_s",
"total_estimated_cost_usd",
"failure_kinds",
"mean_successful_proposers",
"outcomes",
):
assert key in ens, key
# 5 built-in prompts, repeat=1.
assert ens["runs"] == 5
assert len(ens["outcomes"]) == 5
# The ensemble arm reports public-trace proposer counts; baseline does not.
assert ens["mean_successful_proposers"] == 3.0
assert report["baseline"]["mean_successful_proposers"] is None
# Scripted failures: ensemble 1 overload, baseline 2 rate-limits.
assert ens["failures"] == 1
assert report["baseline"]["failures"] == 2
# Cost estimate resolved offline from the static pricing table.
assert ens["total_estimated_cost_usd"] is not None
assert "deltas" in report and "latency_delta_s" in report["deltas"]
def test_bench_dry_run_table_output() -> None:
result = runner.invoke(app, ["ensemble", "bench", "--dry-run"])
assert result.exit_code == 0, result.output
assert "Ensemble benchmark" in result.output
assert "ensemble" in result.output
assert "baseline" in result.output
def test_bench_dry_run_repeat_scales_runs() -> None:
result = runner.invoke(app, ["ensemble", "bench", "--dry-run", "--repeat", "3", "--json"])
assert result.exit_code == 0, result.output
report = json.loads(result.stdout)
assert report["ensemble"]["runs"] == 15 # 5 prompts * 3
def test_bench_dry_run_no_cost_disables_estimate() -> None:
result = runner.invoke(app, ["ensemble", "bench", "--dry-run", "--no-cost", "--json"])
assert result.exit_code == 0, result.output
report = json.loads(result.stdout)
assert report["ensemble"]["total_estimated_cost_usd"] is None
def test_bench_dry_run_custom_prompts_file(tmp_path: Path) -> None:
prompts_file = tmp_path / "prompts.json"
prompts_file.write_text(
json.dumps(
[
{"id": "a", "text": "dummy one"},
{"id": "b", "text": "dummy two", "system": "be terse"},
]
),
encoding="utf-8",
)
result = runner.invoke(
app,
["ensemble", "bench", "--dry-run", "--prompts", str(prompts_file), "--json"],
)
assert result.exit_code == 0, result.output
report = json.loads(result.stdout)
assert report["ensemble"]["runs"] == 2
ids = {outcome["prompt_id"] for outcome in report["ensemble"]["outcomes"]}
assert ids == {"a", "b"}
def test_bench_invalid_prompts_file_exits_two(tmp_path: Path) -> None:
bad = tmp_path / "bad.json"
bad.write_text(textwrap.dedent('{"not": "a list"}'), encoding="utf-8")
result = runner.invoke(app, ["ensemble", "bench", "--dry-run", "--prompts", str(bad)])
assert result.exit_code == 2
combined = result.output + (result.stderr or "")
assert "Invalid prompts file" in combined
@@ -0,0 +1,47 @@
"""Regression: the gateway bind preflight must match uvicorn's real bind.
uvicorn/asyncio sets SO_REUSEADDR on POSIX, so it can bind a port still in
TIME_WAIT after a restart. The preflight probe must do the same or it reports a
false "already in use" when the operator restarts the gateway. It must still
report a truly-live listener as unavailable.
"""
from __future__ import annotations
import socket
import pytest
from opensquilla.cli.gateway_cmd import _gateway_bind_available
def test_bind_available_false_for_live_listener() -> None:
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
port = srv.getsockname()[1]
srv.listen()
try:
assert _gateway_bind_available("127.0.0.1", port) is False
finally:
srv.close()
@pytest.mark.skipif(not hasattr(socket, "SO_REUSEADDR"), reason="needs SO_REUSEADDR")
def test_bind_available_true_after_restart_timewait() -> None:
# Force the port into TIME_WAIT with a real server-side active close, the
# exact state a gateway restart leaves behind. A probe without SO_REUSEADDR
# would report this as "in use"; uvicorn (and now the preflight) binds fine.
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
port = srv.getsockname()[1]
srv.listen()
client = socket.create_connection(("127.0.0.1", port))
conn, _ = srv.accept()
conn.shutdown(socket.SHUT_RDWR)
conn.close()
client.close()
srv.close()
assert _gateway_bind_available("127.0.0.1", port) is True
@@ -0,0 +1,230 @@
from __future__ import annotations
import asyncio
import json
import sys
from types import SimpleNamespace
from typing import Any
import pytest
from opensquilla.cli.gateway_client import (
GatewayClient,
_normalize_session_error_payload,
_task_terminal_as_session_event,
)
_STOP = object()
class _FakeWebSocket:
def __init__(self, recv_frames: list[dict[str, Any]] | None = None) -> None:
self._recv_frames = list(recv_frames or [])
self.sent: list[str] = []
self.iter_queue: asyncio.Queue[str | object] = asyncio.Queue()
self.closed = False
async def recv(self) -> str:
if not self._recv_frames:
raise AssertionError("unexpected recv() call")
return json.dumps(self._recv_frames.pop(0))
async def send(self, payload: str) -> None:
self.sent.append(payload)
async def close(self) -> None:
self.closed = True
def __aiter__(self) -> _FakeWebSocket:
return self
async def __anext__(self) -> str:
item = await self.iter_queue.get()
if item is _STOP:
raise StopAsyncIteration
assert isinstance(item, str)
return item
class _BrokenSendWebSocket:
async def send(self, payload: str) -> None:
raise RuntimeError("socket already closed")
async def _wait_for(predicate, *, timeout: float = 1.0) -> None:
deadline = asyncio.get_running_loop().time() + timeout
while not predicate():
if asyncio.get_running_loop().time() >= deadline:
raise AssertionError("condition was not met before timeout")
await asyncio.sleep(0.005)
def _install_fake_websockets(monkeypatch: pytest.MonkeyPatch, ws: _FakeWebSocket) -> None:
async def _connect(url: str) -> _FakeWebSocket:
return ws
monkeypatch.setitem(sys.modules, "websockets", SimpleNamespace(connect=_connect))
def test_task_terminal_event_uses_terminal_message() -> None:
event = _task_terminal_as_session_event(
"task.timeout",
{
"task_id": "task-1",
"terminal_reason": "timeout",
"terminal_message": "The task timed out before it could finish.",
"error_message": "Gateway task timeout: Stream idle for more than 60s",
},
)
assert event is not None
assert event["event"] == "session.event.error"
assert event["message"] == "The task timed out before it could finish."
assert "Gateway task" not in event["message"]
def test_session_error_payload_is_normalized_for_gateway_client() -> None:
payload = _normalize_session_error_payload(
{
"message": "Iteration 1 exceeded iteration_timeout",
"code": "iteration_timeout",
}
)
assert payload["message"] == "The task timed out before it could finish."
assert payload["terminal_message"] == "The task timed out before it could finish."
assert payload["error_message"] == "The task timed out before it could finish."
def test_gateway_client_error_normalization_preserves_generic_error_detail() -> None:
payload = _normalize_session_error_payload(
{
"message": "Tool failed with exit code 2",
"code": "agent_error",
}
)
assert payload["message"] == "The task failed before it could finish."
assert payload["error_message"] == "Tool failed with exit code 2"
def _handshake_frames(*, keepalive_ms: int = 60_000) -> list[dict[str, Any]]:
return [
{"type": "event", "event": "connect.challenge", "payload": {"nonce": "n"}},
{
"type": "hello-ok",
"policy": {"client_ws_keepalive_timeout_ms": keepalive_ms},
},
]
@pytest.mark.asyncio
async def test_heartbeat_interval_derived_from_hello_policy(
monkeypatch: pytest.MonkeyPatch,
) -> None:
ws = _FakeWebSocket(_handshake_frames(keepalive_ms=60_000))
_install_fake_websockets(monkeypatch, ws)
client = GatewayClient()
await client.connect()
try:
assert client._heartbeat_interval == 24.0 # noqa: SLF001
assert client._heartbeat_task is not None # noqa: SLF001
finally:
await client.close()
@pytest.mark.asyncio
async def test_heartbeat_interval_stays_below_short_hello_policy(
monkeypatch: pytest.MonkeyPatch,
) -> None:
ws = _FakeWebSocket(_handshake_frames(keepalive_ms=2_000))
_install_fake_websockets(monkeypatch, ws)
client = GatewayClient()
await client.connect()
try:
assert client._heartbeat_interval == 0.8 # noqa: SLF001
finally:
await client.close()
@pytest.mark.asyncio
async def test_heartbeat_loop_sends_text_ping_not_rpc() -> None:
ws = _FakeWebSocket()
client = GatewayClient()
client._ws = ws # noqa: SLF001
client._heartbeat_interval = 0.01 # noqa: SLF001
task = asyncio.create_task(client._heartbeat_loop()) # noqa: SLF001
try:
await _wait_for(lambda: bool(ws.sent))
finally:
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
frame = json.loads(ws.sent[0])
assert frame == {"type": "ping"}
assert "id" not in frame
assert "method" not in frame
@pytest.mark.asyncio
async def test_listener_ignores_pong_frames() -> None:
ws = _FakeWebSocket()
client = GatewayClient()
client._ws = ws # noqa: SLF001
task = asyncio.create_task(client._listen()) # noqa: SLF001
try:
await ws.iter_queue.put(json.dumps({"type": "pong"}))
await asyncio.sleep(0.02)
assert client._recv_queue.empty() # noqa: SLF001
finally:
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
@pytest.mark.asyncio
async def test_close_cancels_heartbeat(monkeypatch: pytest.MonkeyPatch) -> None:
ws = _FakeWebSocket(_handshake_frames(keepalive_ms=60_000))
_install_fake_websockets(monkeypatch, ws)
client = GatewayClient()
await client.connect()
heartbeat_task = client._heartbeat_task # noqa: SLF001
assert heartbeat_task is not None
await client.close()
assert heartbeat_task.done()
assert ws.closed is True
@pytest.mark.asyncio
async def test_listener_close_stops_heartbeat_task() -> None:
ws = _FakeWebSocket()
client = GatewayClient()
client._ws = ws # noqa: SLF001
client._heartbeat_interval = 60.0 # noqa: SLF001
client._heartbeat_task = asyncio.create_task(client._heartbeat_loop(ws)) # noqa: SLF001
listener_task = asyncio.create_task(client._listen()) # noqa: SLF001
await ws.iter_queue.put(_STOP)
await _wait_for(lambda: client._connection_error is not None) # noqa: SLF001
assert client._heartbeat_task.done() # noqa: SLF001
await listener_task
@pytest.mark.asyncio
async def test_call_after_send_failure_raises_clear_connection_error() -> None:
client = GatewayClient()
client._ws = _BrokenSendWebSocket() # noqa: SLF001
with pytest.raises(ConnectionError, match="Gateway connection lost"):
await client._call("sessions.messages.subscribe", {"key": "agent:main:x"}) # noqa: SLF001
assert client._pending == {} # noqa: SLF001
File diff suppressed because it is too large Load Diff
+111
View File
@@ -0,0 +1,111 @@
"""``opensquilla gateway reload`` CLI — calls the admin ``config.reload`` RPC.
Offline: the gateway client is replaced by a fake, no network, no credentials.
"""
from __future__ import annotations
import json
from typing import Any
from typer.testing import CliRunner
from opensquilla.cli.main import app
runner = CliRunner()
class FakeGatewayClient:
calls: list[tuple[str, Any]] = []
payload: dict[str, Any] = {}
async def connect(self, url: str, *, token=None) -> None:
type(self).calls.append(("connect", url))
async def close(self) -> None:
type(self).calls.append(("close", None))
async def call(self, method: str, params: dict | None = None) -> Any:
type(self).calls.append((method, params or {}))
return type(self).payload
def _install(monkeypatch, payload: dict[str, Any]) -> type[FakeGatewayClient]:
FakeGatewayClient.calls = []
FakeGatewayClient.payload = payload
monkeypatch.setattr("opensquilla.cli.gateway_client.GatewayClient", FakeGatewayClient)
return FakeGatewayClient
def test_gateway_reload_calls_rpc_and_prints_summary(monkeypatch) -> None:
_install(
monkeypatch,
{
"ok": True,
"path": "/tmp/example-config.toml",
"restartRequired": True,
"restartSections": ["channels"],
"liveApplied": ["naming", "skills"],
},
)
result = runner.invoke(app, ["gateway", "reload"])
assert result.exit_code == 0, result.output
assert ("config.reload", {}) in FakeGatewayClient.calls
assert "naming, skills" in result.stdout
assert "Restart required" in result.stdout
assert "channels" in result.stdout
def test_gateway_reload_no_restart_needed(monkeypatch) -> None:
_install(
monkeypatch,
{
"ok": True,
"path": "/tmp/example-config.toml",
"restartRequired": False,
"restartSections": [],
"liveApplied": [],
},
)
result = runner.invoke(app, ["gateway", "reload"])
assert result.exit_code == 0, result.output
assert "(no changes)" in result.stdout
assert "Restart required" not in result.stdout
def test_gateway_reload_failure_exits_nonzero(monkeypatch) -> None:
_install(
monkeypatch,
{"ok": False, "path": "/tmp/example-config.toml", "error": "bad toml"},
)
result = runner.invoke(app, ["gateway", "reload"])
assert result.exit_code == 1
assert "Reload failed" in result.stderr
assert "bad toml" in result.stderr
assert "left unchanged" in result.stderr
def test_gateway_reload_json_output(monkeypatch) -> None:
_install(
monkeypatch,
{
"ok": True,
"path": "/tmp/example-config.toml",
"restartRequired": False,
"restartSections": [],
"liveApplied": ["naming"],
},
)
result = runner.invoke(app, ["gateway", "reload", "--json"])
assert result.exit_code == 0, result.output
payload = json.loads(result.stdout)
assert payload["ok"] is True
assert payload["liveApplied"] == ["naming"]
+57
View File
@@ -0,0 +1,57 @@
from __future__ import annotations
from opensquilla.cli.gateway_rpc import default_gateway_token, default_gateway_url
def test_default_gateway_url_uses_implicit_home_config(tmp_path, monkeypatch) -> None:
monkeypatch.delenv("OPENSQUILLA_GATEWAY_URL", raising=False)
monkeypatch.delenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", raising=False)
monkeypatch.delenv("OPENSQUILLA_GATEWAY_HOST", raising=False)
monkeypatch.delenv("OPENSQUILLA_GATEWAY_PORT", raising=False)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "state"))
monkeypatch.chdir(tmp_path)
config = tmp_path / "state" / "config.toml"
config.parent.mkdir(parents=True)
config.write_text(
"""
host = "127.0.0.1"
port = 18790
""",
encoding="utf-8",
)
assert default_gateway_url() == "ws://127.0.0.1:18790/ws"
def test_default_gateway_token_uses_explicit_config_path(tmp_path, monkeypatch) -> None:
monkeypatch.delenv("OPENSQUILLA_GATEWAY_TOKEN", raising=False)
monkeypatch.delenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", raising=False)
config = tmp_path / "custom-opensquilla.toml"
config.write_text(
"""
[auth]
mode = "token"
token = "from-explicit-config"
""",
encoding="utf-8",
)
assert default_gateway_token(config) == "from-explicit-config"
def test_default_gateway_token_env_override_wins_over_explicit_config(
tmp_path, monkeypatch
) -> None:
monkeypatch.setenv("OPENSQUILLA_GATEWAY_TOKEN", "from-env")
config = tmp_path / "custom-opensquilla.toml"
config.write_text(
"""
[auth]
mode = "token"
token = "from-explicit-config"
""",
encoding="utf-8",
)
assert default_gateway_token(config) == "from-env"
+191
View File
@@ -0,0 +1,191 @@
"""CLI help presentation tests."""
from __future__ import annotations
import re
from io import StringIO
from pathlib import Path
from types import SimpleNamespace
import click
from rich.console import Console
from typer import rich_utils
from typer.testing import CliRunner
from opensquilla.cli.main import app
from opensquilla.ui import ACCENT, questionary_style
runner = CliRunner()
def test_rich_help_uses_opensquilla_accent() -> None:
assert rich_utils.STYLE_OPTIONS_PANEL_BORDER == ACCENT
assert rich_utils.STYLE_COMMANDS_PANEL_BORDER == ACCENT
assert rich_utils.STYLE_OPTION == f"bold {ACCENT}"
assert rich_utils.STYLE_COMMANDS_TABLE_FIRST_COLUMN == f"bold {ACCENT}"
def test_questionary_checkbox_selection_avoids_reverse_row_highlight() -> None:
style = questionary_style()
assert style is not None
rules = dict(style.style_rules)
for token in ("pointer", "highlighted", "selected"):
assert "noreverse" in rules[token]
assert "bg:" not in rules[token]
def test_onboard_help_keeps_router_option_readable() -> None:
result = runner.invoke(app, ["onboard", "--help"], terminal_width=100)
assert result.exit_code == 0, result.output
output = click.unstyle(result.output)
assert "--router" in output
assert "--router MODE" in output
assert "Router profile: recommended," in output
assert "openrouter-mix, or" in output
assert "disabled." in output
assert "TEXT recommended | openrouter-mix" not in output
def test_onboard_help_uses_compact_option_columns() -> None:
result = runner.invoke(app, ["onboard", "--help"], terminal_width=100)
assert result.exit_code == 0, result.output
output = click.unstyle(result.output)
assert "--provider TEXT" in output
assert "--router MODE" in output
assert not re.search(r"--provider\s{12,}TEXT", output)
def test_onboard_help_explains_first_run_inputs() -> None:
result = runner.invoke(app, ["onboard", "--help"], terminal_width=110)
assert result.exit_code == 0, result.output
output = click.unstyle(result.output)
assert "OpenSquilla setup cockpit" in output
assert "SquillaRouter" in output
assert "Provider id to configure" in output
assert "Model id for the provider" in output
assert "Read the provider key from this environment" in output
assert "variable." in output
assert "Only run the wizard when required setup is" in output
assert "incomplete." in output
assert "Leave channel setup for later" in output
assert "Leave optional Web search setup for later" in output
assert "Leave optional image generation setup for later" in output
def test_configure_help_explains_section_specific_inputs() -> None:
result = runner.invoke(app, ["configure", "--help"], terminal_width=110)
assert result.exit_code == 0, result.output
output = click.unstyle(result.output)
normalized = " ".join(output.replace("", " ").split())
assert (
"Reconfigure provider, router, ensemble, channels, search, image generation, "
"or memory" in normalized
)
assert "Usage:" in output
assert "[SECTION]" in output
assert "SECTION_ARG" not in output
assert "Section to configure" in output
assert "Text provider id for provider setup" in normalized
assert "Search provider id" in normalized
assert "Image provider id" in normalized
assert "Model id for provider or remote memory embedding" in normalized
assert "Channel type such as slack, discord, feishu" in normalized
assert "Image model id" in normalized
assert "Memory embedding provider" in output
def test_help_theme_supports_click_make_metavar_without_context(monkeypatch) -> None:
def legacy_make_metavar(self: click.Option) -> str:
if self.is_bool_flag:
return "BOOLEAN"
return self.metavar or self.type.name.upper()
monkeypatch.setattr(click.Option, "make_metavar", legacy_make_metavar)
result = runner.invoke(app, ["onboard", "--help"], terminal_width=100)
assert result.exit_code == 0, result.output
output = click.unstyle(result.output)
assert "--provider TEXT" in output
assert "--router MODE" in output
def test_help_theme_accepts_typer_vendored_click_parameters() -> None:
option = SimpleNamespace(
param_type_name="option",
name="provider",
opts=["--provider"],
secondary_opts=[],
metavar=None,
type=SimpleNamespace(name="text"),
required=False,
help="Provider id to configure.",
)
argument = SimpleNamespace(
param_type_name="argument",
name="section_arg",
opts=["section_arg"],
secondary_opts=[],
metavar="SECTION",
type=SimpleNamespace(name="text"),
required=False,
help="Section to configure.",
)
console = Console(file=StringIO(), force_terminal=False, color_system=None)
rich_utils._print_options_panel(
name="Options",
params=[option, argument],
ctx=click.Context(click.Command("demo")),
markup_mode="rich",
console=console,
)
output = click.unstyle(console.file.getvalue())
assert "--provider TEXT" in output
assert "SECTION" in output
def test_cli_brand_surfaces_do_not_use_cyan() -> None:
cli_files = [*Path("src/opensquilla/cli").rglob("*.py"), Path("src/opensquilla/ui.py")]
forbidden = (
"bold cyan",
"[cyan]",
"[/cyan]",
"typer.colors.CYAN",
'style="cyan"',
)
offenders: list[str] = []
for path in cli_files:
text = path.read_text(encoding="utf-8")
for needle in forbidden:
if needle in text:
offenders.append(f"{path}:{needle}")
assert offenders == []
def test_opentui_theme_accents_are_not_cyan() -> None:
# The brand's no-cyan rule is about the ACCENT, not secondary semantics: every
# OpenTUI theme's brand accent must be warm/orange, never cyan. (OpenSquilla's
# own --info token is cyan-ish #56C2E6 and is allowed for the route/info role.)
theme_mjs = Path("src/opensquilla/cli/tui/opentui/package/src/theme.mjs").read_text(
encoding="utf-8"
)
block = theme_mjs.split("PALETTES = Object.freeze({", 1)[1].split("});", 1)[0]
accents = re.findall(r"\baccent(?:Secondary)?:\s*\"(#[0-9a-fA-F]{6})\"", block)
def is_cyan(hex_value: str) -> bool:
h = hex_value.lstrip("#")
r, g, b = (int(h[i : i + 2], 16) for i in (0, 2, 4))
return r < 0x80 and g > 0xB0 and b > 0xB0 and abs(g - b) < 0x40
assert accents, "could not parse theme accents from theme.mjs"
offenders = [hex_value for hex_value in accents if is_cyan(hex_value)]
assert offenders == [], f"theme brand accent must not be cyan: {offenders}"
+9
View File
@@ -0,0 +1,9 @@
from opensquilla.cli.init_cmd import _default_model_for_provider
def test_init_uses_direct_deepseek_model_default() -> None:
assert _default_model_for_provider("deepseek") == "deepseek-v4-flash"
def test_init_keeps_openrouter_model_default() -> None:
assert _default_model_for_provider("openrouter") == "deepseek/deepseek-v4-pro"
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
from pathlib import Path
import pytest
import opensquilla.cli.main as cli_main
from opensquilla.gateway.config import GatewayConfig
@pytest.mark.parametrize(
("agent_id", "expected_suffix"),
[
("main", ()),
("ops", ("agents", "ops")),
],
)
def test_cli_dream_uses_configured_agent_workspace(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
agent_id: str,
expected_suffix: tuple[str, ...],
) -> None:
configured_workspace = tmp_path / "configured workspace"
cwd = tmp_path / "launch cwd"
cwd.mkdir()
monkeypatch.chdir(cwd)
cfg = GatewayConfig(workspace_dir=str(configured_workspace))
monkeypatch.setattr(GatewayConfig, "load", classmethod(lambda cls, _path=None: cfg))
dream = cli_main._build_cli_dream(agent_id, need_provider=False)
assert dream.workspace == configured_workspace.joinpath(*expected_suffix)
assert not (cwd / ".opensquilla").exists()
+65
View File
@@ -0,0 +1,65 @@
from __future__ import annotations
from opensquilla.cli.memory_flush_cmd import (
MemoryFlushSessionResult,
_emit_text_result,
_receipt_is_complete_flush,
_zero_usage,
)
def test_receipt_is_complete_flush_rejects_raw_and_degraded_llm() -> None:
assert not _receipt_is_complete_flush(
{
"mode": "raw",
"flushed_paths": ["memory/.raw_fallbacks/raw.md"],
"raw_reason": "timeout",
}
)
assert not _receipt_is_complete_flush(
{
"mode": "llm",
"indexed_chunk_count": 1,
"integrity_status": "missing_chunks",
"output_coverage_status": "ok",
}
)
assert _receipt_is_complete_flush(
{
"mode": "llm",
"indexed_chunk_count": 1,
"integrity_status": "ok",
"output_coverage_status": "ok",
"invalid_candidate_count": 0,
"candidate_missing_ids": [],
"obligation_status": "ok",
"obligation_missing_ids": [],
}
)
def test_emit_text_result_labels_raw_fallback_as_degraded(capsys) -> None:
result = MemoryFlushSessionResult(
ok=False,
key="agent:main:webchat:s1",
agent_id="main",
message_window="all",
flush_max_chars="default",
segment_mode="auto",
segment_max_chars="default",
segment_overlap_messages=0,
flush_receipt={
"mode": "raw",
"flushed_paths": ["memory/.raw_fallbacks/raw.md"],
"raw_reason": "timeout",
},
usage=_zero_usage(),
usage_path=None,
)
_emit_text_result(result, success=False)
captured = capsys.readouterr()
assert "Flush degraded to raw backup" in captured.out
assert "Backup path: memory/.raw_fallbacks/raw.md" in captured.out
assert "not searchable durable memory" in captured.err
+969
View File
@@ -0,0 +1,969 @@
from __future__ import annotations
import json
import os
import sqlite3
import tomllib
from pathlib import Path
import pytest
import tomli_w
from typer.testing import CliRunner
from opensquilla.cli.main import app
runner = CliRunner()
@pytest.fixture(autouse=True)
def _isolate_profile_operation_locks(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Keep CLI migration locks out of the runner's real user-state tree."""
monkeypatch.setenv("OPENSQUILLA_TEST", "1")
monkeypatch.setenv("OPENSQUILLA_USER_STATE_DIR", str(tmp_path / "user-state"))
def _set_fake_home(monkeypatch, home: Path) -> None:
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("USERPROFILE", str(home))
def _make_source(root: Path) -> Path:
source = root / ".openclaw"
workspace = source / "workspace"
workspace.mkdir(parents=True)
(workspace / "SOUL.md").write_text("soul\n", encoding="utf-8")
(workspace / "MEMORY.md").write_text("memory\n", encoding="utf-8")
(source / "openclaw.json").write_text(
json.dumps({"agents": {"defaults": {"model": "deepseek-chat"}}}),
encoding="utf-8",
)
return source
def test_migrate_openclaw_json_dry_run(tmp_path: Path, monkeypatch) -> None:
source = _make_source(tmp_path)
home = tmp_path / "opensquilla-home"
target = tmp_path / "config.toml"
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
result = runner.invoke(
app,
[
"migrate",
"openclaw",
"--source",
str(source),
"--config",
str(target),
"--json",
],
)
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["apply"] is False
assert not target.exists()
assert any(item["status"] == "planned" for item in payload["items"])
def test_migrate_openclaw_apply_writes_config_and_workspace(
tmp_path: Path,
monkeypatch,
) -> None:
source = _make_source(tmp_path)
home = tmp_path / "opensquilla-home"
target = tmp_path / "config.toml"
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
result = runner.invoke(
app,
[
"migrate",
"openclaw",
"--source",
str(source),
"--config",
str(target),
"--apply",
],
)
assert result.exit_code == 0, result.stdout
assert "OpenClaw migration complete" in result.stdout
assert (home / "workspace" / "SOUL.md").read_text(encoding="utf-8") == "soul\n"
config = tomllib.loads(target.read_text(encoding="utf-8"))
assert config["llm"]["model"] == "deepseek-chat"
def test_migrate_openclaw_missing_source_exits_nonzero(tmp_path: Path) -> None:
result = runner.invoke(
app,
[
"migrate",
"openclaw",
"--source",
str(tmp_path / "missing"),
"--json",
],
)
assert result.exit_code == 1
payload = json.loads(result.stdout)
assert payload["items"][0]["status"] == "error"
def test_migrate_openclaw_exclude_skips_workspace_item(
tmp_path: Path,
monkeypatch,
) -> None:
source = _make_source(tmp_path)
home = tmp_path / "opensquilla-home"
target = tmp_path / "config.toml"
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
result = runner.invoke(
app,
[
"migrate",
"openclaw",
"--source",
str(source),
"--config",
str(target),
"--apply",
"--exclude",
"soul",
],
)
assert result.exit_code == 0, result.stdout
assert not (home / "workspace" / "SOUL.md").exists()
config = tomllib.loads(target.read_text(encoding="utf-8"))
assert config["llm"]["model"] == "deepseek-chat"
def test_migrate_openclaw_rejects_unknown_include(tmp_path: Path) -> None:
source = _make_source(tmp_path)
result = runner.invoke(
app,
[
"migrate",
"openclaw",
"--source",
str(source),
"--include",
"not-a-real-option",
],
)
assert result.exit_code != 0
assert "Unknown migration option" in result.stdout
def test_migrate_openclaw_rejects_unknown_preset(tmp_path: Path) -> None:
source = _make_source(tmp_path)
result = runner.invoke(
app,
[
"migrate",
"openclaw",
"--source",
str(source),
"--preset",
"everything",
],
)
assert result.exit_code != 0
assert "Unknown migration preset" in result.stdout
def test_migrate_openclaw_rejects_unknown_skill_conflict(tmp_path: Path) -> None:
source = _make_source(tmp_path)
result = runner.invoke(
app,
[
"migrate",
"openclaw",
"--source",
str(source),
"--skill-conflict",
"merge",
],
)
assert result.exit_code != 0
assert "Unknown skill conflict behavior" in result.stdout
# ---------------------------------------------------------------------------
# OpenSquilla self-migration CLI contract
# ---------------------------------------------------------------------------
def test_migrate_opensquilla_plain_error_reports_failure(tmp_path: Path) -> None:
result = runner.invoke(
app,
["migrate", "opensquilla", "--source", str(tmp_path / "missing")],
)
assert result.exit_code == 1
assert "OpenSquilla self-migration failed" in result.stdout
assert "error: 1" in result.stdout
assert "complete" not in result.stdout.lower()
def test_migrate_opensquilla_json_error_is_parseable_and_exits_nonzero(
tmp_path: Path,
) -> None:
result = runner.invoke(
app,
[
"migrate",
"opensquilla",
"--source",
str(tmp_path / "missing"),
"--json",
],
)
assert result.exit_code == 1
payload = json.loads(result.stdout)
assert payload["items"][0]["status"] == "error"
def test_migrate_opensquilla_rejects_config_with_target_home_guidance(
tmp_path: Path,
) -> None:
result = runner.invoke(
app,
[
"migrate",
"opensquilla",
"--source",
str(tmp_path / "source"),
"--config",
str(tmp_path / "config.toml"),
],
)
assert result.exit_code == 2
assert "--config is not supported for OpenSquilla self-migration" in result.stdout
assert "OPENSQUILLA_STATE_DIR" in result.stdout
assert "target home" in result.stdout
def test_migrate_opensquilla_keeps_its_internal_multi_profile_lock(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla import recovery
from opensquilla.cli import migrate_cmd
source = tmp_path / "source-profile"
(source / "workspace").mkdir(parents=True)
(source / "state").mkdir()
(source / "config.toml").write_text("port = 18790\n", encoding="utf-8")
(source / "workspace" / "SOUL.md").write_text("source soul\n", encoding="utf-8")
target = tmp_path / "desktop-primary"
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(target))
monkeypatch.setenv("OPENSQUILLA_PROFILE_KIND", "desktop-primary")
monkeypatch.delattr(os, "fchmod", raising=False)
def fail_foreign_guard() -> None:
pytest.fail("OpenSquilla self-import must not take the foreign migration lifecycle")
acquired: list[tuple[Path, ...]] = []
original_acquire = recovery.acquire_profile_locks
def track_internal_locks(*homes: str | Path, **kwargs: object) -> object:
acquired.append(tuple(Path(home) for home in homes))
return original_acquire(*homes, **kwargs)
monkeypatch.setattr(migrate_cmd, "_guard_foreign_migration_target", fail_foreign_guard)
monkeypatch.setattr(recovery, "acquire_profile_locks", track_internal_locks)
result = runner.invoke(
app,
[
"migrate",
"opensquilla",
"--source",
str(source),
"--apply",
"--json",
],
)
assert result.exit_code == 0, result.stdout
assert acquired == [(source, target, target.parent)]
assert (target / "workspace" / "SOUL.md").read_text(encoding="utf-8") == (
"source soul\n"
)
# ---------------------------------------------------------------------------
# Auto-detect entry point: ``opensquilla migrate`` (no subcommand)
# ---------------------------------------------------------------------------
def _seed_openclaw(home: Path) -> Path:
source = home / ".openclaw"
workspace = source / "workspace"
workspace.mkdir(parents=True)
(workspace / "SOUL.md").write_text("openclaw soul\n", encoding="utf-8")
(workspace / "MEMORY.md").write_text("openclaw memory\n", encoding="utf-8")
(source / "openclaw.json").write_text("{}", encoding="utf-8")
return source
def _seed_opensquilla(home: Path) -> Path:
source = home / ".opensquilla"
source.mkdir(parents=True)
(source / "config.toml").write_text("port = 18790\n", encoding="utf-8")
return source
def _seed_portable(base: Path) -> Path:
source = base / "OpenSquilla" / "portable" / "dummy-release"
source.mkdir(parents=True)
(source / "config.toml").write_text("port = 18790\n", encoding="utf-8")
return source
def test_migrate_batch_rejects_config_when_opensquilla_is_selected(
tmp_path: Path, monkeypatch
) -> None:
fake_home = tmp_path / "fake-home"
fake_home.mkdir()
_seed_opensquilla(fake_home)
_set_fake_home(monkeypatch, fake_home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "desktop-home"))
result = runner.invoke(
app,
[
"migrate",
"--source",
"opensquilla",
"--config",
str(tmp_path / "custom-config.toml"),
],
)
assert result.exit_code == 2, result.stdout
assert "--config is not supported for OpenSquilla self-migration" in result.stdout
def _seed_hermes(home: Path) -> Path:
source = home / ".hermes"
source.mkdir(parents=True)
(source / "config.yaml").write_text(
"model:\n provider: openrouter\n", encoding="utf-8"
)
(source / "SOUL.md").write_text("hermes soul\n", encoding="utf-8")
return source
@pytest.mark.parametrize(
("source_name", "entrypoint"),
[
("openclaw", "subcommand"),
("hermes", "subcommand"),
("openclaw", "auto-detect"),
("hermes", "auto-detect"),
],
)
def test_foreign_migration_blocks_unsafe_desktop_before_any_write(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
source_name: str,
entrypoint: str,
) -> None:
from opensquilla.recovery import RecoveryRequiredError
fake_home = tmp_path / "fake-home"
fake_home.mkdir()
source_path = (
_seed_openclaw(fake_home)
if source_name == "openclaw"
else _seed_hermes(fake_home)
)
target = tmp_path / "desktop-primary"
target.mkdir()
missing_workspace = tmp_path / "missing-workspace"
config = target / "config.toml"
config_bytes = f"workspace_dir = {json.dumps(str(missing_workspace))}\n".encode()
config.write_bytes(config_bytes)
_set_fake_home(monkeypatch, fake_home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(target))
monkeypatch.setenv("OPENSQUILLA_PROFILE_KIND", "desktop-primary")
if entrypoint == "subcommand":
args = [
"migrate",
source_name,
"--source",
str(source_path),
"--apply",
"--json",
]
else:
args = ["migrate", "--source", source_name, "--apply", "--json"]
result = runner.invoke(app, args)
assert result.exit_code != 0
assert isinstance(result.exception, RecoveryRequiredError)
assert result.exception.report.stable_code == "effective_state_missing"
assert config.read_bytes() == config_bytes
assert not missing_workspace.exists()
assert not (target / "workspace").exists()
assert not (target / "migration").exists()
assert not (target / "state" / "gateway.pid.lock").exists()
assert sorted(path.relative_to(target).as_posix() for path in target.rglob("*")) == [
"config.toml"
]
def test_foreign_migration_guard_is_noop_for_ordinary_cli_profile(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
source = _make_source(tmp_path / "source-root")
target = tmp_path / "cli-home"
target.mkdir()
workspace = tmp_path / "explicit-cli-workspace"
(target / "config.toml").write_text(
tomli_w.dumps({"workspace_dir": str(workspace)}),
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(target))
monkeypatch.delenv("OPENSQUILLA_PROFILE_KIND", raising=False)
monkeypatch.delenv("OPENSQUILLA_DESKTOP", raising=False)
result = runner.invoke(
app,
[
"migrate",
"openclaw",
"--source",
str(source),
"--apply",
"--json",
],
)
assert result.exit_code == 0, result.stdout
assert (workspace / "SOUL.md").read_text(encoding="utf-8") == "soul\n"
assert (target / "migration" / "openclaw").is_dir()
assert not (target / "state" / "gateway.pid.lock").exists()
def test_migrate_auto_detect_no_source_reports_nothing(
tmp_path: Path, monkeypatch
) -> None:
home = tmp_path / "fake_home"
home.mkdir()
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "opensquilla"))
result = runner.invoke(app, ["migrate", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["detected"] == []
assert "No migration source detected" in payload["message"]
assert "CLI, desktop, and portable locations" in payload["message"]
def test_migrate_auto_detect_single_source_auto_picks(
tmp_path: Path, monkeypatch
) -> None:
# Only hermes present: don't prompt, just run it.
home = tmp_path / "fake_home"
home.mkdir()
_seed_hermes(home)
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "opensquilla"))
result = runner.invoke(app, ["migrate", "--apply", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["selected"] == ["hermes"]
assert "hermes" in payload["reports"]
def test_migrate_auto_detect_single_cli_home_still_requires_confirmation(
tmp_path: Path, monkeypatch
) -> None:
home = tmp_path / "fake_home"
home.mkdir()
source = _seed_opensquilla(home)
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "desktop-home"))
result = runner.invoke(app, ["migrate", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["detected"] == [
{
"name": "opensquilla",
"kind": "cli-home",
"path": str(source),
"version": None,
"estimated_activity_at": payload["detected"][0]["estimated_activity_at"],
"session_count": 0,
"size_bytes": payload["detected"][0]["size_bytes"],
"previously_imported": False,
}
]
assert payload["detected"][0]["estimated_activity_at"] is not None
assert payload["detected"][0]["size_bytes"] > 0
assert "explicit" in payload["message"].lower()
assert "--source opensquilla" in payload["message"]
def test_migrate_auto_detect_single_portable_requires_explicit_confirmation(
tmp_path: Path, monkeypatch
) -> None:
home = tmp_path / "fake_home"
home.mkdir()
portable_base = tmp_path / "local-app-data"
portable = _seed_portable(portable_base)
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("LOCALAPPDATA", str(portable_base))
monkeypatch.delenv("TEMP", raising=False)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "opensquilla"))
result = runner.invoke(app, ["migrate", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
candidate = payload["detected"][0]
assert candidate["name"] == "opensquilla"
assert candidate["kind"] == "windows-portable"
assert candidate["path"] == str(portable)
assert candidate["session_count"] == 0
assert candidate["size_bytes"] > 0
assert "estimated_activity_at" in candidate
assert candidate["previously_imported"] is False
assert "Re-run with" in payload["message"]
def test_migrate_opensquilla_single_portable_requires_home_flag(
tmp_path: Path, monkeypatch
) -> None:
portable_base = tmp_path / "local-app-data"
portable = _seed_portable(portable_base)
monkeypatch.setenv("LOCALAPPDATA", str(portable_base))
monkeypatch.delenv("TEMP", raising=False)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "target-home"))
refused = runner.invoke(
app, ["migrate", "opensquilla", "--kind", "windows-portable", "--json"]
)
accepted = runner.invoke(
app,
[
"migrate",
"opensquilla",
"--kind",
"windows-portable",
"--home",
str(portable),
"--json",
],
)
assert refused.exit_code == 2
refused_payload = json.loads(refused.stdout)
assert refused_payload["requires_selection"] is True
assert refused_payload["candidates"][0]["path"] == str(portable)
assert refused_payload["candidates"][0]["session_count"] == 0
assert "estimated_activity_at" in refused_payload["candidates"][0]
assert accepted.exit_code == 0, accepted.stdout
assert json.loads(accepted.stdout)["source"] == str(portable)
@pytest.mark.parametrize("kind", ["cli-home", "desktop-home"])
def test_migrate_opensquilla_direct_same_product_source_requires_confirmation(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
kind: str,
) -> None:
import opensquilla.cli.migrate_cmd as migrate_cmd
fake_home = tmp_path / "fake-home"
fake_home.mkdir()
source = (
_seed_opensquilla(fake_home)
if kind == "cli-home"
else tmp_path / "detected-desktop" / "opensquilla"
)
if kind == "desktop-home":
source.mkdir(parents=True)
(source / "config.toml").write_text("port = 18790\n", encoding="utf-8")
monkeypatch.setattr(migrate_cmd, "detect_desktop_home", lambda: source)
_set_fake_home(monkeypatch, fake_home)
target = tmp_path / "target-home"
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(target))
refused = runner.invoke(
app,
["migrate", "opensquilla", "--kind", kind, "--json"],
)
accepted = runner.invoke(
app,
[
"migrate",
"opensquilla",
"--kind",
kind,
"--source",
str(source),
"--json",
],
)
assert refused.exit_code == 2
payload = json.loads(refused.stdout)
assert payload["requires_selection"] is True
assert payload["candidates"][0]["path"] == str(source)
assert not target.exists()
assert accepted.exit_code == 0, accepted.stdout
assert json.loads(accepted.stdout)["source"] == str(source)
def test_migrate_help_treats_cli_and_desktop_as_supported_profiles() -> None:
group_help = runner.invoke(app, ["migrate", "--help"])
command_help = runner.invoke(app, ["migrate", "opensquilla", "--help"])
assert group_help.exit_code == 0
assert command_help.exit_code == 0
combined = f"{group_help.stdout}\n{command_help.stdout}"
assert "legacy OpenSquilla home" not in combined
assert "supported OpenSquilla CLI or Desktop profile" in combined
assert "historical Windows Portable" in combined
def test_portable_text_chooser_labels_activity_as_estimated_and_shows_metadata(
tmp_path: Path,
monkeypatch,
) -> None:
portable_base = tmp_path / "local-app-data"
portable = _seed_portable(portable_base)
(portable / "install-receipt.json").write_text(
json.dumps({"version": "0.5.0rc3"}),
encoding="utf-8",
)
monkeypatch.setenv("LOCALAPPDATA", str(portable_base))
monkeypatch.delenv("TEMP", raising=False)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "target-home"))
result = runner.invoke(
app,
["migrate", "opensquilla", "--kind", "windows-portable"],
)
assert result.exit_code == 2
assert str(portable) in result.stdout
assert "version 0.5.0rc3" in result.stdout
assert "estimated recent activity" in result.stdout
assert "0 sessions" in result.stdout
assert "bytes" in result.stdout
def test_migrate_opensquilla_inspect_candidate_json_is_metadata_only(
tmp_path: Path,
monkeypatch,
) -> None:
source = _seed_portable(tmp_path / "portable-base")
(source / "install-receipt.json").write_text(
json.dumps({"version": "0.5.0rc3"}),
encoding="utf-8",
)
state = source / "state"
state.mkdir()
connection = sqlite3.connect(state / "sessions.db")
try:
connection.execute("CREATE TABLE sessions (session_key TEXT PRIMARY KEY, title TEXT)")
connection.execute("INSERT INTO sessions VALUES (?, ?)", ("secret-id", "secret-title"))
connection.commit()
finally:
connection.close()
target = tmp_path / "target"
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(target))
result = runner.invoke(
app,
[
"migrate",
"opensquilla",
"--source",
str(source),
"--kind",
"windows-portable",
"--inspect-candidate",
"--json",
],
)
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["candidate"]["version"] == "0.5.0rc3"
assert payload["candidate"]["session_count"] == 1
assert payload["candidate"]["path"] == str(source)
assert "secret-id" not in result.stdout
assert "secret-title" not in result.stdout
assert not target.exists()
def test_migrate_opensquilla_replacement_flags_require_exact_target(
tmp_path: Path, monkeypatch
) -> None:
source = tmp_path / "legacy-home"
source.mkdir()
(source / "config.toml").write_text("port = 18791\n", encoding="utf-8")
(source / "workspace").mkdir()
(source / "state").mkdir()
target = tmp_path / "target-home"
target.mkdir()
(target / "existing.txt").write_text("preserve", encoding="utf-8")
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(target))
refused = runner.invoke(
app,
[
"migrate",
"opensquilla",
"--source",
str(source),
"--apply",
"--overwrite",
"--json",
],
)
accepted = runner.invoke(
app,
[
"migrate",
"opensquilla",
"--source",
str(source),
"--apply",
"--replace-target",
"--confirm-replace-target",
str(target.resolve()),
"--json",
],
)
assert refused.exit_code == 1
assert "exact confirmation" in refused.stdout
assert accepted.exit_code == 0, accepted.stdout
assert not (target / "existing.txt").exists()
assert (target / "config.toml").is_file()
def test_migrate_auto_detect_multiple_sources_non_tty_lists_and_exits(
tmp_path: Path, monkeypatch
) -> None:
# Both sources present and no --source filter: in non-TTY (CliRunner)
# the user must opt in explicitly. We print the discovered sources
# and exit 0 so CI doesn't silently migrate things.
home = tmp_path / "fake_home"
home.mkdir()
_seed_openclaw(home)
_seed_hermes(home)
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "opensquilla"))
result = runner.invoke(app, ["migrate", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
detected_names = [entry["name"] for entry in payload["detected"]]
assert detected_names == ["openclaw", "hermes"]
assert "Re-run with" in payload["message"]
def test_migrate_auto_detect_source_filter_runs_only_selected(
tmp_path: Path, monkeypatch
) -> None:
home = tmp_path / "fake_home"
home.mkdir()
_seed_openclaw(home)
_seed_hermes(home)
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "opensquilla"))
result = runner.invoke(
app, ["migrate", "--source", "hermes", "--apply", "--json"]
)
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["selected"] == ["hermes"]
assert "openclaw" not in payload["reports"]
def test_migrate_auto_detect_source_filter_runs_both_in_order(
tmp_path: Path, monkeypatch
) -> None:
home = tmp_path / "fake_home"
home.mkdir()
_seed_openclaw(home)
_seed_hermes(home)
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "opensquilla"))
result = runner.invoke(
app,
["migrate", "--source", "hermes,openclaw", "--apply", "--json"],
)
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
# Order is canonical (openclaw first, then hermes) regardless of how
# the user wrote the --source flag, so the second migrator sees
# whatever the first one wrote.
assert payload["selected"] == ["openclaw", "hermes"]
assert set(payload["reports"]) == {"openclaw", "hermes"}
def test_migrate_auto_detect_rejects_unknown_source_name(
tmp_path: Path, monkeypatch
) -> None:
home = tmp_path / "fake_home"
home.mkdir()
_seed_openclaw(home)
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "opensquilla"))
result = runner.invoke(app, ["migrate", "--source", "bogus", "--json"])
assert result.exit_code == 2, result.stdout
assert "Unknown migration source" in result.stdout
def test_migrate_auto_detect_rejects_requested_but_undetected_source(
tmp_path: Path, monkeypatch
) -> None:
# ``--source hermes`` when hermes is not on disk should fail loudly
# rather than silently no-op.
home = tmp_path / "fake_home"
home.mkdir()
_seed_openclaw(home)
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "opensquilla"))
result = runner.invoke(app, ["migrate", "--source", "hermes", "--json"])
assert result.exit_code == 2, result.stdout
assert "not detected" in result.stdout
def test_migrate_auto_detect_tty_prompt_path_is_invoked(
tmp_path: Path, monkeypatch
) -> None:
# When both sources are present and stdin is a TTY (real interactive
# use), we should reach the questionary prompt instead of the
# non-TTY exit branch. Patch the prompt helper so the test doesn't
# need a real terminal. ``--json`` short-circuits to the non-TTY
# branch on purpose (scripting context), so this test uses plain
# text output and checks that the migration actually ran via the
# files it wrote.
home = tmp_path / "fake_home"
home.mkdir()
_seed_openclaw(home)
_seed_hermes(home)
state = tmp_path / "opensquilla"
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(state))
from opensquilla.cli import migrate_cmd
monkeypatch.setattr("opensquilla.cli.migrate_cmd._stdin_is_tty", lambda: True)
captured: list[list[object]] = []
def fake_prompt(detected):
captured.append(list(detected))
# User picks only hermes.
return ["hermes"]
monkeypatch.setattr(migrate_cmd, "_prompt_source_selection", fake_prompt)
result = runner.invoke(app, ["migrate", "--apply"])
assert result.exit_code == 0, result.stdout
assert "hermes migration complete" in result.stdout
# openclaw was offered but the fake prompt only picked hermes, so
# the openclaw migrator must NOT have run.
assert "openclaw migration complete" not in result.stdout
assert len(captured) == 1
assert {source.name for source in captured[0]} == {"openclaw", "hermes"}
def test_migrate_auto_detect_validates_all_selected_before_running_any(
tmp_path: Path, monkeypatch
) -> None:
# Pre-validate so an invalid flag for the second migrator never
# half-applies the first one. ``persona_conflict`` is the openclaw-only
# flag, so a bogus value for it must error out even though hermes
# would happily ignore it.
home = tmp_path / "fake_home"
home.mkdir()
_seed_openclaw(home)
_seed_hermes(home)
state = tmp_path / "opensquilla"
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(state))
result = runner.invoke(
app,
[
"migrate",
"--source",
"openclaw,hermes",
"--persona-conflict",
"absolutely-bogus",
"--apply",
],
)
assert result.exit_code == 2, result.stdout
assert "Unknown persona conflict behavior" in result.stdout
# Neither migrator should have left state behind: the workspace dir
# is created by the first migrator's apply, so its absence is proof
# we bailed before running anything.
assert not (state / "workspace").exists()
def test_migrate_auto_detect_tty_prompt_cancellation_exits_cleanly(
tmp_path: Path, monkeypatch
) -> None:
home = tmp_path / "fake_home"
home.mkdir()
_seed_openclaw(home)
_seed_hermes(home)
_set_fake_home(monkeypatch, home)
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "opensquilla"))
from opensquilla.cli import migrate_cmd
monkeypatch.setattr("opensquilla.cli.migrate_cmd._stdin_is_tty", lambda: True)
monkeypatch.setattr(migrate_cmd, "_prompt_source_selection", lambda _detected: [])
result = runner.invoke(app, ["migrate"])
assert result.exit_code == 0, result.stdout
assert "No source selected" in result.stdout
+323
View File
@@ -0,0 +1,323 @@
"""CLI tests for `opensquilla models probe` (offline, injected/fake results).
The probe command is live by nature, so these tests never let it reach a
network: probe results are injected by monkeypatching the shared onboarding
probe helpers, and the two real-path cases (missing key, unknown provider id)
short-circuit inside validation before any provider is contacted.
"""
from __future__ import annotations
import json
import textwrap
from pathlib import Path
from typing import Any
from typer.testing import CliRunner
from opensquilla.cli.main import app
from opensquilla.onboarding.probe import ProviderModelsDiscoverResult, ProviderProbeResult
runner = CliRunner()
# Synthetic sentinel; never a real credential. If redaction ever regresses,
# this exact token would leak into the rendered output and fail the tests.
SENTINEL_SECRET = "sk-test-sentinel-000000000000" # noqa: S105 - synthetic dummy
def _write_config(tmp_path: Path, body: str) -> Path:
path = tmp_path / "config.toml"
path.write_text(textwrap.dedent(body), encoding="utf-8")
return path
def _primary_openai_config(tmp_path: Path) -> Path:
return _write_config(
tmp_path,
"""
[llm]
provider = "openai"
model = "gpt-test-dummy"
api_key = "sk-test-dummy-key"
""",
)
def _fake_probe(results: dict[str, ProviderProbeResult], calls: list[dict[str, Any]]):
async def fake(**kwargs: Any) -> ProviderProbeResult:
calls.append(kwargs)
return results[kwargs["provider_id"]]
return fake
def _fake_discover(
results: dict[str, ProviderModelsDiscoverResult], calls: list[dict[str, Any]]
):
async def fake(**kwargs: Any) -> ProviderModelsDiscoverResult:
calls.append(kwargs)
return results[kwargs["provider_id"]]
return fake
def test_probe_ok_renders_table_and_exits_zero(tmp_path: Path, monkeypatch) -> None:
config = _primary_openai_config(tmp_path)
calls: list[dict[str, Any]] = []
monkeypatch.setattr(
"opensquilla.cli.models_cmd.probe_llm_provider",
_fake_probe(
{"openai": ProviderProbeResult(ok=True, provider_id="openai", model="gpt-test-dummy")},
calls,
),
)
result = runner.invoke(app, ["models", "probe", "--config", str(config)])
assert result.exit_code == 0, result.output
assert "openai" in result.output
assert "gpt-test-dummy" in result.output
assert "ok" in result.output
assert len(calls) == 1
assert calls[0]["provider_id"] == "openai"
assert calls[0]["model"] == "gpt-test-dummy"
def test_probe_failure_classifies_and_exits_nonzero(tmp_path: Path, monkeypatch) -> None:
config = _primary_openai_config(tmp_path)
monkeypatch.setattr(
"opensquilla.cli.models_cmd.probe_llm_provider",
_fake_probe(
{
"openai": ProviderProbeResult(
ok=False,
provider_id="openai",
model="gpt-test-dummy",
failure_kind="transport_transient",
message="injected connection timeout",
)
},
[],
),
)
result = runner.invoke(app, ["models", "probe", "--config", str(config)])
assert result.exit_code == 1
assert "transport_transient" in result.output
assert "injected connection timeout" in result.output
def test_probe_redacts_sentinel_secret_from_error_detail(
tmp_path: Path, monkeypatch
) -> None:
config = _primary_openai_config(tmp_path)
poisoned = ProviderProbeResult(
ok=False,
provider_id="openai",
model="gpt-test-dummy",
failure_kind="auth_invalid",
message=f"Invalid api_key={SENTINEL_SECRET} rejected (Bearer {SENTINEL_SECRET})",
code="401",
)
monkeypatch.setattr(
"opensquilla.cli.models_cmd.probe_llm_provider",
_fake_probe({"openai": poisoned}, []),
)
table_result = runner.invoke(app, ["models", "probe", "--config", str(config)])
json_result = runner.invoke(
app, ["models", "probe", "--config", str(config), "--json"]
)
assert table_result.exit_code == 1
assert json_result.exit_code == 1
assert "auth_invalid" in table_result.output
assert SENTINEL_SECRET not in table_result.output
assert SENTINEL_SECRET not in json_result.output
def test_probe_json_shape(tmp_path: Path, monkeypatch) -> None:
config = _primary_openai_config(tmp_path)
monkeypatch.setattr(
"opensquilla.cli.models_cmd.probe_llm_provider",
_fake_probe(
{
"openai": ProviderProbeResult(
ok=False,
provider_id="openai",
model="gpt-test-dummy",
failure_kind="rate_limited",
message="injected rate limit",
code="429",
latency_ms=123,
)
},
[],
),
)
result = runner.invoke(app, ["models", "probe", "--config", str(config), "--json"])
assert result.exit_code == 1
rows = json.loads(result.stdout)
assert isinstance(rows, list) and len(rows) == 1
row = rows[0]
assert row["provider"] == "openai"
assert row["model"] == "gpt-test-dummy"
assert row["ok"] is False
assert row["kind"] == "rate_limited"
assert row["detail"] == "injected rate limit"
assert row["code"] == "429"
assert row["method"] == "chat"
assert row["source"] == "llm"
assert row["latency_ms"] == 123
def test_probe_unknown_provider_filter_exits_two(tmp_path: Path) -> None:
config = _primary_openai_config(tmp_path)
result = runner.invoke(
app,
["models", "probe", "--config", str(config), "--provider", "not-configured"],
)
assert result.exit_code == 2
combined = result.output + (result.stderr or "")
assert "not configured" in combined.lower()
def test_probe_missing_key_classifies_auth_invalid_offline(tmp_path: Path) -> None:
# Real probe path (no monkeypatch): the conftest strips provider env keys
# and the config has none, so probe_llm_provider short-circuits with
# AUTH_INVALID before any provider is even built — fully offline.
config = _write_config(
tmp_path,
"""
[llm]
provider = "openai"
model = "gpt-test-dummy"
""",
)
result = runner.invoke(app, ["models", "probe", "--config", str(config)])
assert result.exit_code == 1
assert "auth_invalid" in result.output
assert "No API key available" in result.output
def test_probe_unknown_provider_id_reports_invalid_config(tmp_path: Path) -> None:
# Real probe path: an unregistered provider id fails spec validation
# before any network contact.
config = _write_config(
tmp_path,
"""
[llm]
provider = "not-a-real-provider"
model = "dummy-model"
""",
)
result = runner.invoke(app, ["models", "probe", "--config", str(config)])
assert result.exit_code == 1
assert "invalid_config" in result.output
def test_probe_profile_without_tier_model_uses_models_list(
tmp_path: Path, monkeypatch
) -> None:
config = _write_config(
tmp_path,
"""
[llm]
provider = "openai"
model = "gpt-test-dummy"
api_key = "sk-test-dummy-key"
[llm_profiles.anthropic]
api_key = "sk-test-dummy-profile-key"
""",
)
probe_calls: list[dict[str, Any]] = []
discover_calls: list[dict[str, Any]] = []
monkeypatch.setattr(
"opensquilla.cli.models_cmd.probe_llm_provider",
_fake_probe(
{"openai": ProviderProbeResult(ok=True, provider_id="openai", model="gpt-test-dummy")},
probe_calls,
),
)
monkeypatch.setattr(
"opensquilla.cli.models_cmd.discover_provider_models",
_fake_discover(
{
"anthropic": ProviderModelsDiscoverResult(
ok=True, provider_id="anthropic", source="live"
)
},
discover_calls,
),
)
result = runner.invoke(app, ["models", "probe", "--config", str(config), "--json"])
assert result.exit_code == 0, result.output
rows = json.loads(result.stdout)
by_provider = {row["provider"]: row for row in rows}
assert set(by_provider) == {"openai", "anthropic"}
assert by_provider["openai"]["method"] == "chat"
assert by_provider["anthropic"]["method"] == "models_list"
assert by_provider["anthropic"]["source"] == "llm_profiles"
assert by_provider["anthropic"]["latency_ms"] == 0
assert [call["provider_id"] for call in probe_calls] == ["openai"]
assert [call["provider_id"] for call in discover_calls] == ["anthropic"]
def test_probe_provider_filter_and_model_override(tmp_path: Path, monkeypatch) -> None:
config = _write_config(
tmp_path,
"""
[llm]
provider = "openai"
model = "gpt-test-dummy"
api_key = "sk-test-dummy-key"
[llm_profiles.anthropic]
api_key = "sk-test-dummy-profile-key"
""",
)
calls: list[dict[str, Any]] = []
monkeypatch.setattr(
"opensquilla.cli.models_cmd.probe_llm_provider",
_fake_probe(
{
"openai": ProviderProbeResult(
ok=True, provider_id="openai", model="override-model-dummy"
)
},
calls,
),
)
result = runner.invoke(
app,
[
"models",
"probe",
"--config",
str(config),
"--provider",
"openai",
"--model",
"override-model-dummy",
"--json",
],
)
assert result.exit_code == 0, result.output
rows = json.loads(result.stdout)
assert len(rows) == 1 # the filter drops the profile target
assert rows[0]["model"] == "override-model-dummy"
assert calls[0]["model"] == "override-model-dummy"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,863 @@
"""Headless `onboard`/`configure` CLI semantics.
Pins the gate-flag error contract (explicit-but-incomplete flag sets exit 2),
keep-current re-saves, cancellation and config-error productization, restart
guidance, the voice-audio catalog section, and strict `--field` coercion.
All tests are offline and use synthetic dummy data only.
"""
from __future__ import annotations
import re
import pytest
from typer.testing import CliRunner
from opensquilla.cli.main import app
from opensquilla.onboarding.config_store import load_config
runner = CliRunner()
_ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
def _plain(value: str) -> str:
return " ".join(_ANSI_RE.sub("", value).split())
# ---------------------------------------------------------------------------
# B2-1: explicit-but-incomplete headless flag combinations must exit 2 and
# name the missing gate flag instead of silently no-opping with exit 0.
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
("args", "missing_flag"),
[
(["configure", "router", "--default-tier", "c2"], "--router"),
(["configure", "provider", "--model", "some/model"], "--provider"),
(["configure", "search", "--max-results", "9"], "--search-provider"),
(["configure", "channels", "--channel-type", "slack"], "--name"),
(["configure", "channels", "--name", "work"], "--channel-type"),
(["configure", "image", "--primary", "openrouter/x"], "--image-provider"),
(["configure", "memory", "--onnx-dir", "models/bge"], "--memory-provider"),
],
)
def test_configure_incomplete_flags_exit_2_naming_missing_gate(
tmp_path, monkeypatch, args, missing_flag
):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard", *args])
assert result.exit_code == 2, result.output
assert missing_flag in _plain(result.stderr)
assert "no changes were made" in _plain(result.stderr)
assert not target.exists()
def test_configure_router_incomplete_flags_do_not_touch_existing_config(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
target.write_text(
'[llm]\nprovider = "openrouter"\nmodel = "m"\napi_key = "sk"\n',
encoding="utf-8",
)
before = target.read_text(encoding="utf-8")
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard", "configure", "router", "--default-tier", "c2"])
assert result.exit_code == 2
assert target.read_text(encoding="utf-8") == before
def test_configure_flags_without_section_exit_2(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard", "configure", "--router", "recommended"])
assert result.exit_code == 2
assert "target section" in _plain(result.stderr)
assert not target.exists()
def test_configure_unknown_section_exits_2(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard", "configure", "not-a-section"])
assert result.exit_code == 2
assert "unknown configure section" in _plain(result.stderr)
assert "not-a-section" in _plain(result.stderr)
assert not target.exists()
def test_configure_audio_section_exits_2_with_catalog_pointer(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard", "configure", "audio"])
assert result.exit_code == 2
assert "opensquilla onboard catalog audio" in _plain(result.stderr)
assert not target.exists()
def test_configure_bare_non_tty_exits_2_with_hint(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard", "configure"])
assert result.exit_code == 2
assert "requires a TTY" in result.stdout
assert not target.exists()
# ---------------------------------------------------------------------------
# B2-2: `onboard catalog audio` must render usable output, and the overview
# must not leak the raw audioProviders key.
# ---------------------------------------------------------------------------
def test_onboard_catalog_audio_prints_provider_rows(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard", "catalog", "audio"])
assert result.exit_code == 0, result.stdout
assert result.stdout.strip()
assert "voice audio provider options" in result.stdout
assert "elevenlabs" in result.stdout
assert "ELEVENLABS_API_KEY" in result.stdout
assert "Try:" in result.stdout
assert not target.exists()
def test_onboard_catalog_overview_names_voice_audio_without_raw_key(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard", "catalog"])
assert result.exit_code == 0, result.stdout
assert "Voice audio providers" in result.stdout
assert "audioProviders" not in result.stdout
compact = "".join(result.stdout.split())
assert "opensquillaonboardcatalogaudio" in compact
# ---------------------------------------------------------------------------
# B2-3: an explicitly stored enabled=false must survive an image key rotation
# that omits --image-enabled/--no-image-enabled.
# ---------------------------------------------------------------------------
def test_configure_image_rotation_keeps_deliberate_disabled_state(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
target.write_text(
'[llm]\nprovider = "openrouter"\nmodel = "m"\napi_key = "sk"\n'
"\n"
"[image_generation]\n"
"enabled = false\n"
'primary = "openrouter/google/gemini-3.1-flash-image-preview"\n'
"\n"
"[image_generation.providers.openrouter]\n"
'api_key_env = "OPENSQUILLA_TEST_IMAGE_KEY"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setenv("OPENSQUILLA_TEST_IMAGE_KEY", "sk-image-env")
result = runner.invoke(
app,
[
"onboard",
"configure",
"image",
"--image-provider",
"openrouter",
"--primary",
"openrouter/google/gemini-3.1-flash-image-preview",
"--api-key-env",
"OPENSQUILLA_TEST_IMAGE_KEY",
],
)
assert result.exit_code == 0, result.output
assert load_config(target).image_generation.enabled is False
def test_configure_image_rotation_keeps_enabled_true(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
target.write_text(
"[image_generation]\n"
"enabled = true\n"
'primary = "openrouter/google/gemini-3.1-flash-image-preview"\n'
"\n"
"[image_generation.providers.openrouter]\n"
'api_key_env = "OPENSQUILLA_TEST_IMAGE_KEY"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setenv("OPENSQUILLA_TEST_IMAGE_KEY", "sk-image-env")
result = runner.invoke(
app,
[
"onboard",
"configure",
"image",
"--image-provider",
"openrouter",
"--primary",
"openrouter/google/gemini-3.1-flash-image-preview",
"--api-key-env",
"OPENSQUILLA_TEST_IMAGE_KEY",
],
)
assert result.exit_code == 0, result.output
assert load_config(target).image_generation.enabled is True
# ---------------------------------------------------------------------------
# B2-4: key rotation must not clobber stored provider/search settings when
# the corresponding flags are omitted.
# ---------------------------------------------------------------------------
def test_configure_provider_key_rotation_keeps_model_base_url_and_proxy(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "custom/model-x"\n'
'api_key = "sk-old"\n'
'base_url = "https://gateway.example.test/v1"\n'
'proxy = "http://127.0.0.1:7890"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
[
"onboard",
"configure",
"provider",
"--provider",
"openrouter",
"--api-key",
"sk-new",
],
)
assert result.exit_code == 0, result.output
cfg = load_config(target)
assert cfg.llm.model == "custom/model-x"
assert cfg.llm.base_url == "https://gateway.example.test/v1"
assert cfg.llm.proxy == "http://127.0.0.1:7890"
assert cfg.llm.api_key == "sk-new"
def test_onboard_provider_key_rotation_keeps_base_url_and_proxy(
tmp_path, monkeypatch
):
# The top-level `onboard --provider` path must honor the same
# keep-current contract as `configure provider`: an omitted --router
# never re-applies a router profile on a configured install, so the
# stored model and endpoint settings all survive a key rotation.
target = tmp_path / "c.toml"
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "custom/model-x"\n'
'api_key = "sk-old"\n'
'base_url = "https://gateway.example.test/v1"\n'
'proxy = "http://127.0.0.1:7890"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", "sk-new", "--minimal"],
)
assert result.exit_code == 0, result.output
cfg = load_config(target)
assert cfg.llm.model == "custom/model-x"
assert cfg.llm.base_url == "https://gateway.example.test/v1"
assert cfg.llm.proxy == "http://127.0.0.1:7890"
assert cfg.llm.api_key == "sk-new"
def test_onboard_provider_key_rotation_with_router_disabled_keeps_model(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "custom/model-x"\n'
'api_key = "sk-old"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
[
"onboard",
"--provider",
"openrouter",
"--api-key",
"sk-new",
"--router",
"disabled",
"--minimal",
],
)
assert result.exit_code == 0, result.output
cfg = load_config(target)
assert cfg.llm.model == "custom/model-x"
assert cfg.llm.api_key == "sk-new"
def test_configure_search_key_rotation_keeps_stored_global_settings(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
target.write_text(
'search_provider = "brave"\n'
'search_api_key = "sk-old"\n'
"search_max_results = 9\n"
'search_proxy = "http://127.0.0.1:7890"\n'
"search_use_env_proxy = true\n"
'search_fallback_policy = "network"\n'
"search_diagnostics = true\n",
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
[
"onboard",
"configure",
"search",
"--search-provider",
"brave",
"--api-key",
"sk-new",
],
)
assert result.exit_code == 0, result.output
cfg = load_config(target)
assert cfg.search_max_results == 9
assert cfg.search_proxy == "http://127.0.0.1:7890"
assert cfg.search_use_env_proxy is True
assert cfg.search_fallback_policy == "network"
assert cfg.search_diagnostics is True
assert cfg.search_api_key == "sk-new"
def test_configure_search_explicit_flags_still_override_stored(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
target.write_text(
'search_provider = "duckduckgo"\n'
"search_max_results = 9\n"
'search_proxy = "http://127.0.0.1:7890"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
[
"onboard",
"configure",
"search",
"--search-provider",
"duckduckgo",
"--max-results",
"3",
"--proxy",
"",
],
)
assert result.exit_code == 0, result.output
cfg = load_config(target)
assert cfg.search_max_results == 3
assert cfg.search_proxy == ""
# ---------------------------------------------------------------------------
# B2-5: a wizard cancellation (Esc/Ctrl+C) must exit with one short line and
# code 130 instead of a raw traceback.
# ---------------------------------------------------------------------------
def test_onboard_wizard_cancellation_is_productized(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
from opensquilla.onboarding.errors import UserCancelledError
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def cancelled(_options):
raise UserCancelledError(section="provider")
monkeypatch.setattr(onboard_cmd, "run_interactive_onboard", cancelled)
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 130
assert "Setup cancelled" in _plain(result.stderr)
assert "Traceback" not in result.output + result.stderr
assert not target.exists()
def test_configure_wizard_cancellation_is_productized(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
from opensquilla.onboarding.errors import UserCancelledError
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def cancelled(_section, *, config_path=None):
raise UserCancelledError(section="configure")
monkeypatch.setattr(onboard_cmd, "run_interactive_configure", cancelled)
result = runner.invoke(app, ["onboard", "configure", "router"])
assert result.exit_code == 130
assert "Setup cancelled" in _plain(result.stderr)
assert "Traceback" not in result.output + result.stderr
assert not target.exists()
# ---------------------------------------------------------------------------
# B2-6: bare `onboard` with a corrupt config must route through the
# productized config-error handoff (exit 2), never a raw traceback.
# ---------------------------------------------------------------------------
def test_bare_onboard_toml_decode_error_is_productized(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
target.write_text("not toml :::", encoding="utf-8")
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 2
assert "OpenSquilla config error" in result.stderr
assert "Traceback" not in result.output + result.stderr
def test_bare_onboard_validation_error_is_productized(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
target.write_text("[search]\n", encoding="utf-8")
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 2
assert "OpenSquilla config error" in result.stderr
assert "pydantic_core" not in result.stderr
assert "Traceback" not in result.output + result.stderr
def test_bare_onboard_os_error_is_productized(tmp_path, monkeypatch):
target = tmp_path / "config-dir.toml"
target.mkdir() # opening a directory raises IsADirectoryError (OSError)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 2
assert "OpenSquilla config error" in result.stderr
assert "Traceback" not in result.output + result.stderr
def test_onboard_provider_with_corrupt_config_is_productized(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
target.write_text("not toml :::", encoding="utf-8")
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", "sk", "--minimal"],
)
assert result.exit_code == 2
assert "OpenSquilla config error" in result.stderr
assert "Traceback" not in result.output + result.stderr
# ---------------------------------------------------------------------------
# B2-7: a ValidationError on the `onboard --provider` path must not echo
# pydantic's input_value (which can carry a mispasted secret).
# ---------------------------------------------------------------------------
def test_onboard_provider_validation_error_never_echoes_input_value(
tmp_path, monkeypatch
):
from pydantic import BaseModel, ValidationError
from opensquilla.cli import onboard_cmd
class _Probe(BaseModel):
max_tokens: int
try:
_Probe(max_tokens="sk-super-secret-value") # type: ignore[arg-type]
except ValidationError as exc:
captured = exc
else: # pragma: no cover - the construction above always fails
raise AssertionError("expected a ValidationError")
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
class _RaisingEngine:
def __init__(self, *_a, **_kw):
pass
def apply(self, *_a, **_kw):
raise captured
def persist(self, *_a, **_kw): # pragma: no cover - apply raises first
raise AssertionError("persist must not run after a failed apply")
monkeypatch.setattr(onboard_cmd, "SetupEngine", _RaisingEngine)
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", "sk", "--minimal"],
)
assert result.exit_code == 2
combined = result.output + result.stderr
assert "sk-super-secret-value" not in combined
assert "input_value" not in combined
assert "max_tokens" in _plain(result.stderr)
assert "Traceback" not in combined
# ---------------------------------------------------------------------------
# B2-8: headless channels/memory saves must surface PersistResult's
# restart_required instead of printing only the saved path.
# ---------------------------------------------------------------------------
def test_configure_channels_prints_restart_guidance(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
[
"onboard",
"configure",
"channels",
"--channel-type",
"slack",
"--name",
"work",
"--token",
"xoxb-secret",
"--field",
"signing_secret=ss",
],
)
assert result.exit_code == 0, result.output
plain = _plain(result.stdout)
assert "restart required" in plain
assert "opensquilla gateway restart" in plain
def test_configure_memory_prints_restart_guidance_with_config_path(
tmp_path, monkeypatch
):
default_target = tmp_path / "default.toml"
target = tmp_path / "custom.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(default_target))
result = runner.invoke(
app,
[
"onboard",
"configure",
"memory",
"--memory-provider",
"local",
"--onnx-dir",
"models/bge",
"--config",
str(target),
],
)
assert result.exit_code == 0, result.output
plain = _plain(result.stdout)
assert "restart required" in plain
assert "opensquilla gateway restart" in plain
assert str(target) in plain
assert not default_target.exists()
def test_configure_search_does_not_print_restart_guidance(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
["onboard", "configure", "search", "--search-provider", "duckduckgo"],
)
assert result.exit_code == 0, result.output
assert "restart required" not in _plain(result.stdout)
# ---------------------------------------------------------------------------
# B2-10: strict --field coercion (bool typos and numeric garbage must name
# the offending field and the accepted spellings).
# ---------------------------------------------------------------------------
def test_configure_channels_field_bool_typo_exits_2_naming_field(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
[
"onboard",
"configure",
"channels",
"--channel-type",
"slack",
"--name",
"work",
"--token",
"xoxb-secret",
"--field",
"signing_secret=ss",
"--field",
"enabled=ture",
],
)
assert result.exit_code == 2
plain = _plain(result.output + result.stderr)
assert "enabled" in plain
assert "true/false" in plain
assert "'ture'" in plain
assert not target.exists()
def test_configure_channels_field_int_garbage_exits_2_naming_field(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
[
"onboard",
"configure",
"channels",
"--channel-type",
"discord",
"--name",
"guild",
"--token",
"bot-secret",
"--field",
"intents=lots",
],
)
assert result.exit_code == 2
plain = _plain(result.output + result.stderr)
assert "intents" in plain
assert "integer" in plain
assert not target.exists()
def test_parse_channel_field_pairs_strict_coercion_unit():
import typer
from opensquilla.cli.channel_fields import parse_channel_field_pairs
assert parse_channel_field_pairs(["enabled=TRUE"], "slack")["enabled"] is True
assert parse_channel_field_pairs(["enabled=off"], "slack")["enabled"] is False
with pytest.raises(typer.BadParameter, match=r"--field enabled .*'ture'"):
parse_channel_field_pairs(["enabled=ture"], "slack")
with pytest.raises(typer.BadParameter, match=r"--field intents .*integer"):
parse_channel_field_pairs(["intents=lots"], "discord")
with pytest.raises(typer.BadParameter, match=r"--field poll_idle_sleep_s .*number"):
parse_channel_field_pairs(["poll_idle_sleep_s=fast"], "telegram")
# ---------------------------------------------------------------------------
# B2-11: previously untested branches.
# ---------------------------------------------------------------------------
def test_onboard_probe_flag_exits_nonzero_when_probe_raises(tmp_path, monkeypatch):
from opensquilla.onboarding import probe as probe_module
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
async def exploding_probe(**_kwargs):
raise RuntimeError("socket exploded mid-flight")
monkeypatch.setattr(probe_module, "probe_llm_provider", exploding_probe)
result = runner.invoke(
app,
[
"onboard",
"--provider",
"openrouter",
"--model",
"deepseek/deepseek-v4-flash",
"--api-key",
"sk",
"--minimal",
"--probe",
],
)
assert result.exit_code == 1
assert "Probe failed" in result.stderr
assert "socket exploded mid-flight" in result.stderr
assert "Traceback" not in result.output + result.stderr
# The probe is a gate, not a rollback: the save sticks.
assert "openrouter" in target.read_text()
def test_onboard_catalog_unknown_section_exits_2(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard", "catalog", "not-a-section"])
assert result.exit_code == 2
assert "unknown setup section" in _plain(result.stderr)
assert "Traceback" not in result.output + result.stderr
def test_onboard_if_needed_names_unfinished_sections(tmp_path, monkeypatch):
# has_config=True but the referenced env key is missing: the gate must
# explain which sections are unfinished before falling into the wizard.
target = tmp_path / "c.toml"
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "deepseek/deepseek-v4-flash"\n'
'api_key_env = "CUSTOM_LLM_KEY"\n',
encoding="utf-8",
)
monkeypatch.delenv("CUSTOM_LLM_KEY", raising=False)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard", "--if-needed"])
assert result.exit_code == 2 # non-TTY still exits 2 afterwards
assert "onboarding has unfinished sections" in result.stdout
assert "Provider" in result.stdout
def test_bare_configure_hub_exit_without_changes_is_success(tmp_path, monkeypatch):
import sys
import types
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
from opensquilla.onboarding import flow
monkeypatch.setattr(flow, "_is_tty", lambda: True)
class _Answer:
def __init__(self, value):
self.value = value
def ask(self):
return self.value
class _Questionary(types.SimpleNamespace):
def select(self, message: str, **kwargs):
assert message == "Section"
assert kwargs["choices"][-1] == "Exit (nothing changed)"
return _Answer("Exit (nothing changed)")
def text(self, message: str, **_kwargs):
raise AssertionError(f"unexpected text prompt: {message}")
def confirm(self, message: str, **_kwargs):
raise AssertionError(f"unexpected confirm prompt: {message}")
def password(self, message: str, **_kwargs):
raise AssertionError(f"unexpected password prompt: {message}")
def checkbox(self, message: str, **_kwargs):
raise AssertionError(f"unexpected checkbox prompt: {message}")
monkeypatch.setitem(sys.modules, "questionary", _Questionary())
result = runner.invoke(app, ["onboard", "configure"])
assert result.exit_code == 0, result.output
assert "requires a TTY" not in result.stdout
assert not target.exists()
# ---------------------------------------------------------------------------
# B2-9: router tier ids in the CLI stay sourced from the router_tiers
# helpers instead of raw tuples/literals.
# ---------------------------------------------------------------------------
def test_router_catalog_tracks_router_tier_helpers(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
from opensquilla.router_tiers import DEFAULT_TEXT_TIER, TEXT_TIERS
assert (
f"--default-tier {DEFAULT_TEXT_TIER}"
in onboard_cmd._CATALOG_COMMANDS["routerProfiles"]
)
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(app, ["onboard", "catalog", "router"])
assert result.exit_code == 0, result.stdout
for tier in TEXT_TIERS:
assert f"{tier}:" in result.stdout
@@ -0,0 +1,474 @@
"""Regression tests for review findings on the headless onboard CLI.
Covers: keep-current --router on re-saves (S1), EOFError cancellation
productization (S4), restart guidance at the bare-onboard boundary (F17),
wizard-phase OSError diagnosis (F27), validation-error redaction (F29), and
the single-preflight-load contract of the --provider path (F41).
All tests are offline and use synthetic dummy data only.
"""
from __future__ import annotations
import errno
import re
import tomllib
import pytest
from typer.testing import CliRunner
from opensquilla.cli.main import app
from opensquilla.onboarding.config_store import load_config
runner = CliRunner()
_ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
def _plain(value: str) -> str:
return " ".join(_ANSI_RE.sub("", value).split())
# ---------------------------------------------------------------------------
# S1: bare `onboard --provider` re-saves keep the stored router state when
# --router is omitted; explicit --router and first-run behavior are unchanged.
# ---------------------------------------------------------------------------
def test_onboard_provider_key_rotation_keeps_router_disabled_and_model(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "custom/model-x"\n'
'api_key = "sk-old"\n'
"\n"
"[squilla_router]\n"
"enabled = false\n",
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", "sk-new", "--minimal"],
)
assert result.exit_code == 0, result.output
cfg = load_config(target)
assert cfg.squilla_router.enabled is False
assert cfg.llm.model == "custom/model-x"
assert cfg.llm.api_key == "sk-new"
data = tomllib.loads(target.read_text(encoding="utf-8"))
assert data["squilla_router"]["enabled"] is False
def test_onboard_provider_key_rotation_keeps_hand_customized_tiers(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "custom/model-x"\n'
'api_key = "sk-old"\n'
"\n"
"[squilla_router.tiers.c0]\n"
'provider = "openrouter"\n'
'model = "my-org/custom-c0"\n'
"[squilla_router.tiers.c1]\n"
'provider = "openrouter"\n'
'model = "my-org/custom-c1"\n'
"[squilla_router.tiers.c2]\n"
'provider = "openrouter"\n'
'model = "my-org/custom-c2"\n'
"[squilla_router.tiers.c3]\n"
'provider = "openrouter"\n'
'model = "my-org/custom-c3"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", "sk-new", "--minimal"],
)
assert result.exit_code == 0, result.output
cfg = load_config(target)
for tier in ("c0", "c1", "c2", "c3"):
assert cfg.squilla_router.tiers[tier]["model"] == f"my-org/custom-{tier}"
assert cfg.llm.api_key == "sk-new"
def test_onboard_provider_explicit_router_flag_still_applies_on_resave(
tmp_path, monkeypatch
):
target = tmp_path / "c.toml"
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "custom/model-x"\n'
'api_key = "sk-old"\n'
"\n"
"[squilla_router]\n"
"enabled = false\n",
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
[
"onboard",
"--provider",
"openrouter",
"--api-key",
"sk-new",
"--router",
"recommended",
"--minimal",
],
)
assert result.exit_code == 0, result.output
cfg = load_config(target)
assert cfg.squilla_router.enabled is True
assert cfg.squilla_router.tier_profile == "openrouter"
def test_onboard_provider_first_run_still_applies_recommended_router(
tmp_path, monkeypatch
):
# Fresh/unconfigured install: an omitted --router keeps today's first-run
# behavior and applies the recommended profile.
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.delenv("OPENSQUILLA_LLM_API_KEY", raising=False)
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", "sk", "--minimal"],
)
assert result.exit_code == 0, result.output
data = tomllib.loads(target.read_text(encoding="utf-8"))
assert data["squilla_router"]["tier_profile"] == "openrouter"
cfg = load_config(target)
assert cfg.squilla_router.enabled is True
def test_onboard_provider_first_run_with_env_key_still_applies_router(
tmp_path, monkeypatch
):
# An env-absorbed credential must not make a fresh install look
# configured: the recommended profile still applies on first setup.
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setenv("OPENSQUILLA_LLM_API_KEY", "sk-from-env")
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", "sk", "--minimal"],
)
assert result.exit_code == 0, result.output
data = tomllib.loads(target.read_text(encoding="utf-8"))
assert data["squilla_router"]["tier_profile"] == "openrouter"
# ---------------------------------------------------------------------------
# S4: EOFError (Ctrl+D / exhausted piped stdin) must be productized exactly
# like Esc/Ctrl+C — one short "Setup cancelled" line, exit 130.
# ---------------------------------------------------------------------------
def test_onboard_wizard_eof_is_productized_as_cancellation(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def eof(_options):
raise EOFError
monkeypatch.setattr(onboard_cmd, "run_interactive_onboard", eof)
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 130
assert "Setup cancelled" in _plain(result.stderr)
assert "Aborted" not in result.output + result.stderr
assert "Traceback" not in result.output + result.stderr
assert not target.exists()
def test_configure_wizard_eof_is_productized_as_cancellation(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def eof(_section, *, config_path=None):
raise EOFError
monkeypatch.setattr(onboard_cmd, "run_interactive_configure", eof)
result = runner.invoke(app, ["onboard", "configure", "router"])
assert result.exit_code == 130
assert "Setup cancelled" in _plain(result.stderr)
assert "Aborted" not in result.output + result.stderr
assert "Traceback" not in result.output + result.stderr
assert not target.exists()
# ---------------------------------------------------------------------------
# F17: bare `onboard` must surface PersistResult.restart_required with the
# same structured guidance `onboard configure` prints.
# ---------------------------------------------------------------------------
def test_bare_onboard_prints_restart_guidance_when_result_requires_it(
tmp_path, monkeypatch
):
from opensquilla.cli import onboard_cmd
from opensquilla.onboarding.config_store import PersistResult
target = tmp_path / "c.toml"
target.write_text(
'[llm]\nprovider = "openrouter"\nmodel = "m"\napi_key = "sk"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def hub_edit(_options):
# Simulates onboard -> "Change specific sections" -> a channel edit:
# the aggregated result carries the sticky restart flag.
return PersistResult(
path=target,
backup_path=None,
restart_required=True,
warnings=[],
)
monkeypatch.setattr(onboard_cmd, "run_interactive_onboard", hub_edit)
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 0, result.output
plain = _plain(result.stdout)
assert "restart required" in plain
assert "opensquilla gateway restart" in plain
def test_bare_onboard_prints_no_restart_guidance_without_flag(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
from opensquilla.onboarding.config_store import PersistResult
target = tmp_path / "c.toml"
target.write_text(
'[llm]\nprovider = "openrouter"\nmodel = "m"\napi_key = "sk"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.setattr(
onboard_cmd,
"run_interactive_onboard",
lambda _options: PersistResult(
path=target,
backup_path=None,
restart_required=False,
warnings=[],
),
)
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 0, result.output
assert "restart required" not in _plain(result.stdout)
# ---------------------------------------------------------------------------
# F27: an OSError raised INSIDE the wizard (e.g. disk full during the final
# persist) is a write failure, not a config-load error — the CLI must not
# claim the config failed to load or suggest editing/moving it.
# ---------------------------------------------------------------------------
def test_onboard_wizard_write_oserror_is_not_misdiagnosed_as_load_error(
tmp_path, monkeypatch
):
from opensquilla.cli import onboard_cmd
target = tmp_path / "c.toml"
target.write_text(
'[llm]\nprovider = "openrouter"\nmodel = "m"\napi_key = "sk"\n',
encoding="utf-8",
)
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def disk_full(_options):
raise OSError(errno.ENOSPC, "No space left on device")
monkeypatch.setattr(onboard_cmd, "run_interactive_onboard", disk_full)
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 2
plain = _plain(result.stderr)
assert "No space left on device" in plain
assert "OpenSquilla config error" not in plain
assert "edit or move this config" not in plain
assert "Traceback" not in result.output + result.stderr
def test_onboard_prevalidates_config_before_entering_wizard(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
target = tmp_path / "c.toml"
target.write_text("not toml :::", encoding="utf-8")
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def must_not_run(_options): # pragma: no cover - the pre-check exits first
raise AssertionError("the wizard must not start over a corrupt config")
monkeypatch.setattr(onboard_cmd, "run_interactive_onboard", must_not_run)
result = runner.invoke(app, ["onboard"])
assert result.exit_code == 2
assert "OpenSquilla config error" in result.stderr
# ---------------------------------------------------------------------------
# F29: a validator message that interpolates the offending input must be
# masked by the free-text redactor before it reaches stderr.
# ---------------------------------------------------------------------------
def test_onboard_provider_validation_error_redacts_secret_shaped_values(
tmp_path, monkeypatch
):
from pydantic import BaseModel, ValidationError, field_validator
from opensquilla.cli import onboard_cmd
secret = "sk-live-SECRETSECRETSECRETSECRET123456"
class _Probe(BaseModel):
api_key: str
@field_validator("api_key")
@classmethod
def _reject(cls, value: str) -> str:
raise ValueError(f"key {value!r} looks malformed")
try:
_Probe(api_key=secret)
except ValidationError as exc:
captured = exc
else: # pragma: no cover - the construction above always fails
raise AssertionError("expected a ValidationError")
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
class _RaisingEngine:
def __init__(self, *_a, **_kw):
pass
def apply(self, *_a, **_kw):
raise captured
def persist(self, *_a, **_kw): # pragma: no cover - apply raises first
raise AssertionError("persist must not run after a failed apply")
monkeypatch.setattr(onboard_cmd, "SetupEngine", _RaisingEngine)
result = runner.invoke(
app,
["onboard", "--provider", "openrouter", "--api-key", secret, "--minimal"],
)
assert result.exit_code == 2
combined = result.output + result.stderr
assert secret not in combined
assert "***" in _plain(result.stderr)
assert "api_key" in _plain(result.stderr)
assert not target.exists()
def test_format_validation_error_masks_interpolated_secret_unit():
from pydantic import BaseModel, ValidationError, field_validator
from opensquilla.cli.onboard_cmd import _format_validation_error
secret = "sk-live-UNITSECRETUNITSECRET987654"
class _Probe(BaseModel):
api_key: str
@field_validator("api_key")
@classmethod
def _reject(cls, value: str) -> str:
raise ValueError(f"key {value!r} looks malformed")
with pytest.raises(ValidationError) as excinfo:
_Probe(api_key=secret)
rendered = _format_validation_error(excinfo.value)
assert secret not in rendered
assert "***" in rendered
assert "api_key" in rendered
# ---------------------------------------------------------------------------
# F41: the `onboard --provider` path must reuse its preflight load for the
# save instead of loading the config again inside the engine.
# ---------------------------------------------------------------------------
def test_onboard_provider_reuses_preflight_load_for_the_save(tmp_path, monkeypatch):
from opensquilla.cli import onboard_cmd
from opensquilla.onboarding import setup_engine
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
def engine_must_not_load(*_a, **_kw): # pragma: no cover - regression guard
raise AssertionError(
"SetupEngine must receive the preflight-loaded config, not reload it"
)
monkeypatch.setattr(setup_engine, "load_config", engine_must_not_load)
load_calls: list[object] = []
real_load = onboard_cmd.load_config
def counting_load(path=None):
load_calls.append(path)
return real_load(path)
monkeypatch.setattr(onboard_cmd, "load_config", counting_load)
result = runner.invoke(
app,
[
"onboard",
"--provider",
"openrouter",
"--model",
"deepseek/deepseek-v4-flash",
"--api-key",
"sk",
"--minimal",
],
)
assert result.exit_code == 0, result.output
# One preflight load feeding the engine + one post-save load for the
# handoff summary; the previous shape performed three full loads.
assert len(load_calls) == 2
assert "openrouter" in target.read_text(encoding="utf-8")
+153
View File
@@ -0,0 +1,153 @@
"""Shape freeze for ``opensquilla onboard status --json``.
The CLI JSON payload must stay a strict superset of the RPC
``onboarding.status`` payload (contract-frozen in
tests/test_contracts/test_onboarding_status.py): every RPC key appears here
under the same name, and ``sectionAliases`` (plus the ``provider`` alias
entries inside ``sections``/``sectionDetails``) is the only CLI-side
addition. Renaming or dropping a key is a contract break and must fail here;
adding one requires consciously extending the frozen set below.
"""
from __future__ import annotations
import json as _json
from typer.testing import CliRunner
from opensquilla.cli.main import app
runner = CliRunner()
# RPC onboarding.status keys (mirrors STATUS_TOP_LEVEL_KEYS in
# tests/test_contracts/test_onboarding_status.py) — every one of these must
# also appear in the CLI JSON payload.
RPC_STATUS_KEYS = frozenset(
{
"configPath",
"hasConfig",
"llmConfigured",
"llmSource",
"llmEnvKey",
"llmCredentialStatus",
"imageGenerationConfigured",
"imageGenerationEnabled",
"imageGenerationSource",
"imageGenerationProvider",
"imageGenerationPrimary",
"imageGenerationEnvKey",
"audioConfigured",
"audioEnabled",
"audioSource",
"audioProvider",
"audioEnvKey",
"searchConfigured",
"searchProvider",
"searchSource",
"searchEnvKey",
"memoryEmbeddingConfigured",
"memoryEmbeddingProvider",
"memoryEmbeddingSource",
"memoryEmbeddingEnvKey",
"channelCount",
"channelsConfigured",
"ensembleCredentialStatus",
"needsOnboarding",
"sections",
"sectionDetails",
"envRecoveryCommands",
"warnings",
# Nullable legacy-data advisory block (see the deliberate additive
# extension in tests/test_contracts/test_onboarding_status.py).
"legacyData",
}
)
# Exact CLI JSON shape: the RPC keys plus the CLI-only alias map.
CLI_STATUS_KEYS = RPC_STATUS_KEYS | {"sectionAliases"}
def _write_config(tmp_path, monkeypatch):
target = tmp_path / "config.toml"
target.write_text(
"[llm]\n"
'provider = "openrouter"\n'
'model = "deepseek/deepseek-v4-flash"\n'
'api_key_env = "DUMMY_UNSET_LLM_KEY"\n',
encoding="utf-8",
)
monkeypatch.delenv("DUMMY_UNSET_LLM_KEY", raising=False)
return target
def _status_json(target):
result = runner.invoke(app, ["onboard", "status", "--json", "--config", str(target)])
assert result.exit_code == 0, result.output
return _json.loads(result.stdout)
def test_status_json_key_set_is_frozen(tmp_path, monkeypatch):
payload = _status_json(_write_config(tmp_path, monkeypatch))
assert set(payload) == CLI_STATUS_KEYS
def test_status_json_is_a_superset_of_the_rpc_payload(tmp_path, monkeypatch):
"""Anti-drift: compare against the live RPC payload, not a copied list."""
from opensquilla.gateway.rpc import RpcContext
from opensquilla.gateway.rpc_onboarding import _status_payload as rpc_status_payload
from opensquilla.onboarding.config_store import load_config
target = _write_config(tmp_path, monkeypatch)
cli_payload = _status_json(target)
rpc_payload = rpc_status_payload(
RpcContext(conn_id="cli-freeze", config=load_config(target))
)
missing = set(rpc_payload) - set(cli_payload)
assert not missing, f"CLI status --json lost RPC keys: {sorted(missing)}"
assert set(cli_payload) - set(rpc_payload) == {"sectionAliases"}
def test_status_json_new_keys_carry_the_expected_values(tmp_path, monkeypatch):
payload = _status_json(_write_config(tmp_path, monkeypatch))
assert payload["llmConfigured"] is False
assert payload["llmSource"] == "missing_env"
credential = payload["llmCredentialStatus"]
assert credential["provider"] == "openrouter"
assert credential["available"] is False
assert credential["source"] == "missing_env"
assert credential["envKey"] == "DUMMY_UNSET_LLM_KEY"
assert payload["audioConfigured"] is False
assert payload["audioEnabled"] is False
# Default search provider (duckduckgo) needs no key, so the section is
# already configured on a fresh config.
assert payload["searchConfigured"] is True
assert payload["channelsConfigured"] is False
assert isinstance(payload["ensembleCredentialStatus"], list)
assert isinstance(payload["warnings"], list)
def test_status_json_command_field_is_bare_on_posix(tmp_path, monkeypatch):
from opensquilla.onboarding import next_steps
monkeypatch.setattr(next_steps.platform, "system", lambda: "Linux")
payload = _status_json(_write_config(tmp_path, monkeypatch))
commands = payload["envRecoveryCommands"]
assert commands
assert commands[0]["command"] == 'export DUMMY_UNSET_LLM_KEY="<your-key>"'
def test_status_json_command_field_has_no_shell_label_on_windows(tmp_path, monkeypatch):
"""Machine-readable command fields must contain only the command."""
from opensquilla.onboarding import next_steps
monkeypatch.setattr(next_steps.platform, "system", lambda: "Windows")
payload = _status_json(_write_config(tmp_path, monkeypatch))
commands = payload["envRecoveryCommands"]
assert commands
assert commands[0]["command"] == '$env:DUMMY_UNSET_LLM_KEY = "<your-key>"'
assert all("PowerShell" not in entry["command"] for entry in commands)
+29
View File
@@ -0,0 +1,29 @@
from __future__ import annotations
import json
from opensquilla.cli.output import emit_error, print_json
def test_print_json_uses_stdout(capsys):
print_json({"text": "héllo", "value": object()})
captured = capsys.readouterr()
payload = json.loads(captured.out)
assert payload["text"] == "héllo"
assert captured.err == ""
def test_emit_error_json_uses_stderr(capsys):
emit_error("bad input", json_output=True, code="INVALID_REQUEST", details={"field": "x"})
captured = capsys.readouterr()
assert captured.out == ""
payload = json.loads(captured.err)
assert payload == {
"error": {
"message": "bad input",
"code": "INVALID_REQUEST",
"details": {"field": "x"},
}
}
+260
View File
@@ -0,0 +1,260 @@
"""Regression tests for CLI profile resolution before home .env loading."""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
from typer.testing import CliRunner
from opensquilla.cli.main import app
from opensquilla.paths import default_opensquilla_home
runner = CliRunner()
def _write_profile(home: Path, name: str, env_lines: list[str]) -> Path:
profile_dir = home / name
profile_dir.mkdir(parents=True, exist_ok=True)
(profile_dir / ".env").write_text("\n".join(env_lines) + "\n", encoding="utf-8")
return profile_dir
def test_cli_profile_loads_selected_profile_env(monkeypatch, tmp_path: Path) -> None:
home = tmp_path / "profiles"
_write_profile(home, "default", ["PROFILE_MARK=loaded-default"])
_write_profile(home, "coder", ["CODER_MARK=loaded-coder"])
for key in (
"OPENSQUILLA_HOME",
"OPENSQUILLA_PROFILE",
"OPENSQUILLA_STATE_DIR",
"PROFILE_MARK",
"CODER_MARK",
):
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv("OPENSQUILLA_HOME", str(home))
result = runner.invoke(app, ["--profile", "coder", "providers", "list", "--json"])
assert result.exit_code == 0, result.output
assert os.environ["OPENSQUILLA_PROFILE"] == "coder"
assert os.environ["CODER_MARK"] == "loaded-coder"
assert "PROFILE_MARK" not in os.environ
assert default_opensquilla_home() == home / "coder"
def test_cli_without_profile_keeps_legacy_home(monkeypatch, tmp_path: Path) -> None:
legacy_home = tmp_path / ".opensquilla"
legacy_home.mkdir()
(legacy_home / ".env").write_text("LEGACY_MARK=loaded\n", encoding="utf-8")
for key in (
"OPENSQUILLA_HOME",
"OPENSQUILLA_PROFILE",
"OPENSQUILLA_STATE_DIR",
"LEGACY_MARK",
):
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv("HOME", str(tmp_path))
result = runner.invoke(app, ["providers", "list", "--json"])
assert result.exit_code == 0, result.output
assert os.environ["LEGACY_MARK"] == "loaded"
assert default_opensquilla_home() == legacy_home
def test_cli_does_not_reload_same_home_env_after_key_removed(
monkeypatch, tmp_path: Path
) -> None:
legacy_home = tmp_path / ".opensquilla"
legacy_home.mkdir()
(legacy_home / ".env").write_text("RELOAD_MARK=loaded\n", encoding="utf-8")
for key in (
"OPENSQUILLA_HOME",
"OPENSQUILLA_PROFILE",
"OPENSQUILLA_STATE_DIR",
"RELOAD_MARK",
):
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv("HOME", str(tmp_path))
result = runner.invoke(app, ["providers", "list", "--json"])
assert result.exit_code == 0, result.output
assert os.environ["RELOAD_MARK"] == "loaded"
monkeypatch.delenv("RELOAD_MARK", raising=False)
result = runner.invoke(app, ["providers", "list", "--json"])
assert result.exit_code == 0, result.output
assert "RELOAD_MARK" not in os.environ
def test_cli_rejects_invalid_profile(monkeypatch) -> None:
monkeypatch.delenv("OPENSQUILLA_STATE_DIR", raising=False)
result = runner.invoke(app, ["--profile", "../escape", "providers", "list"])
assert result.exit_code != 0
assert "lowercase letters" in result.output
def test_env_load_uses_provided_home(monkeypatch, tmp_path: Path) -> None:
from opensquilla.env import load_env
home = tmp_path / "custom"
home.mkdir(parents=True)
(home / ".env").write_text("CUSTOM_MARK=ok\n", encoding="utf-8")
for key in (
"OPENSQUILLA_HOME",
"OPENSQUILLA_PROFILE",
"OPENSQUILLA_STATE_DIR",
"CUSTOM_MARK",
):
monkeypatch.delenv(key, raising=False)
assert load_env(home=home) >= 1
assert os.environ["CUSTOM_MARK"] == "ok"
def test_cli_profile_env_wins_over_legacy_home_for_same_key(
monkeypatch, tmp_path: Path
) -> None:
legacy_home = tmp_path / ".opensquilla"
profiles_root = legacy_home / "profiles"
legacy_home.mkdir()
(legacy_home / ".env").write_text("PROFILE_SHARED_MARK=legacy\n", encoding="utf-8")
_write_profile(profiles_root, "coder", ["PROFILE_SHARED_MARK=coder"])
for key in (
"OPENSQUILLA_HOME",
"OPENSQUILLA_PROFILE",
"OPENSQUILLA_STATE_DIR",
"PROFILE_SHARED_MARK",
):
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv("HOME", str(tmp_path))
cwd = tmp_path / "cwd"
cwd.mkdir()
monkeypatch.chdir(cwd)
result = runner.invoke(app, ["--profile", "coder", "providers", "list", "--json"])
assert result.exit_code == 0, result.output
assert os.environ["PROFILE_SHARED_MARK"] == "coder"
assert default_opensquilla_home() == profiles_root / "coder"
def test_cli_profile_env_wins_on_cold_start_import(tmp_path: Path) -> None:
repo_root = Path(__file__).resolve().parents[2]
cwd = tmp_path / "cwd"
cwd.mkdir()
legacy_home = tmp_path / ".opensquilla"
profiles_root = legacy_home / "profiles"
legacy_home.mkdir()
(legacy_home / ".env").write_text("PROFILE_SHARED_MARK=legacy\n", encoding="utf-8")
_write_profile(profiles_root, "coder", ["PROFILE_SHARED_MARK=coder"])
env = os.environ.copy()
env.update({"HOME": str(tmp_path)})
env.pop("OPENSQUILLA_HOME", None)
env.pop("OPENSQUILLA_PROFILE", None)
env.pop("OPENSQUILLA_STATE_DIR", None)
env.pop("PROFILE_SHARED_MARK", None)
pythonpath = [
str(repo_root / "src"),
str(repo_root),
env.get("PYTHONPATH", ""),
]
env["PYTHONPATH"] = os.pathsep.join(path for path in pythonpath if path)
script = """
import os
import sys
from typer.testing import CliRunner
sys.argv = ["opensquilla", "--profile", "coder", "providers", "list", "--json"]
from opensquilla.cli.main import app
cold_start_mark = os.environ.get("PROFILE_SHARED_MARK", "")
if cold_start_mark != "coder":
print(cold_start_mark)
raise SystemExit(2)
result = CliRunner().invoke(app, ["--profile", "coder", "providers", "list", "--json"])
if result.exit_code != 0:
print(result.output)
raise SystemExit(result.exit_code)
print(os.environ.get("PROFILE_SHARED_MARK", ""))
raise SystemExit(0 if os.environ.get("PROFILE_SHARED_MARK") == "coder" else 2)
"""
result = subprocess.run(
[sys.executable, "-c", script],
cwd=cwd,
env=env,
text=True,
capture_output=True,
check=False,
)
assert result.returncode == 0, result.stdout + result.stderr
def test_cli_profile_from_cwd_env_wins_over_legacy_home_on_cold_start(
tmp_path: Path,
) -> None:
repo_root = Path(__file__).resolve().parents[2]
cwd = tmp_path / "cwd"
cwd.mkdir()
(cwd / ".env").write_text("OPENSQUILLA_PROFILE=coder\n", encoding="utf-8")
legacy_home = tmp_path / ".opensquilla"
profiles_root = legacy_home / "profiles"
legacy_home.mkdir()
(legacy_home / ".env").write_text("PROFILE_SHARED_MARK=legacy\n", encoding="utf-8")
_write_profile(profiles_root, "coder", ["PROFILE_SHARED_MARK=coder"])
env = os.environ.copy()
env.update({"HOME": str(tmp_path)})
env.pop("OPENSQUILLA_HOME", None)
env.pop("OPENSQUILLA_PROFILE", None)
env.pop("OPENSQUILLA_STATE_DIR", None)
env.pop("PROFILE_SHARED_MARK", None)
pythonpath = [
str(repo_root / "src"),
str(repo_root),
env.get("PYTHONPATH", ""),
]
env["PYTHONPATH"] = os.pathsep.join(path for path in pythonpath if path)
script = """
import os
from opensquilla.paths import default_opensquilla_home
from opensquilla.cli.main import app
if os.environ.get("PROFILE_SHARED_MARK") != "coder":
print(os.environ.get("PROFILE_SHARED_MARK", ""))
raise SystemExit(2)
if default_opensquilla_home().name != "coder":
print(default_opensquilla_home())
raise SystemExit(3)
raise SystemExit(0)
"""
result = subprocess.run(
[sys.executable, "-c", script],
cwd=cwd,
env=env,
text=True,
capture_output=True,
check=False,
)
assert result.returncode == 0, result.stdout + result.stderr
+97
View File
@@ -0,0 +1,97 @@
"""CLI tests for `opensquilla providers`."""
from __future__ import annotations
from pathlib import Path
from typer.testing import CliRunner
from opensquilla.cli.main import app
runner = CliRunner()
def test_providers_list_shows_all_supported(tmp_path: Path, monkeypatch):
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(tmp_path / "c.toml"))
result = runner.invoke(app, ["providers", "list"])
assert result.exit_code == 0
out = result.stdout
for pid in ("openrouter", "openai", "ollama", "vllm", "azure"):
assert pid in out
def test_providers_list_marks_unsupported(tmp_path, monkeypatch):
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(tmp_path / "c.toml"))
result = runner.invoke(app, ["providers", "list"])
assert result.exit_code == 0
assert "openai_codex" in result.stdout
assert "unsupported" in result.stdout.lower() or "disabled" in result.stdout.lower()
def test_providers_configure_writes_config(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
[
"providers", "configure", "openrouter",
"--model", "deepseek/deepseek-v4-flash",
"--api-key", "sk-test",
],
)
assert result.exit_code == 0, result.stdout
text = target.read_text()
assert "openrouter" in text
assert "deepseek/deepseek-v4-flash" in text
assert "sk-test" not in result.stdout
def test_providers_configure_unsupported_fails(tmp_path, monkeypatch):
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(tmp_path / "c.toml"))
result = runner.invoke(
app, ["providers", "configure", "github_copilot", "--model", "x"]
)
assert result.exit_code != 0
assert (
"not runtime-supported" in result.stdout.lower()
or "not runtime-supported" in (result.stderr or "").lower()
)
def test_providers_configure_ollama_no_key_required(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app, ["providers", "configure", "ollama", "--model", "llama3"]
)
assert result.exit_code == 0
assert "ollama" in target.read_text()
def test_providers_configure_vllm_requires_base_url(tmp_path, monkeypatch):
# vllm is experimental (registry-runnable, unverified): configurable, but
# its explicit base_url requirement still validates.
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(tmp_path / "c.toml"))
result = runner.invoke(
app,
["providers", "configure", "vllm", "--model", "x", "--api-key", "k"],
)
assert result.exit_code != 0
combined = (result.stdout + (result.stderr or "")).lower()
assert "base_url" in combined
result = runner.invoke(
app,
[
"providers",
"configure",
"vllm",
"--model",
"x",
"--api-key",
"k",
"--base-url",
"http://localhost:8000/v1",
],
)
assert result.exit_code == 0
+115
View File
@@ -0,0 +1,115 @@
"""``opensquilla router calibrate`` CLI.
Seeds a synthetic ``router_decisions`` table (hand-created — never real state)
and drives the command through Typer's ``CliRunner``. The output file always
lands under a monkeypatched ``OPENSQUILLA_STATE_DIR`` temp dir, never the real
home.
"""
from __future__ import annotations
import json
import sqlite3
from pathlib import Path
from typing import Any
from typer.testing import CliRunner
from opensquilla.cli.main import app
from opensquilla.persistence.router_decision_writer import RouterDecisionWriter
runner = CliRunner()
_CREATE_TABLE = (
"CREATE TABLE router_decisions ("
" decision_id TEXT PRIMARY KEY, session_key TEXT NOT NULL,"
" turn_index INTEGER, ts_ms INTEGER NOT NULL, classifier TEXT,"
" proposed_tier TEXT, confidence REAL, probs TEXT, flags TEXT,"
" final_tier TEXT, provider TEXT, model TEXT, thinking_level TEXT,"
" source TEXT, trail TEXT, baseline_model TEXT, savings_pct REAL,"
" executed_kind TEXT, ensemble_profile TEXT,"
" fallback_hops INTEGER NOT NULL DEFAULT 0)"
)
def _seed_db(path: Path, *, count: int = 40) -> None:
conn = sqlite3.connect(str(path), check_same_thread=False, isolation_level=None)
conn.row_factory = sqlite3.Row
conn.execute(_CREATE_TABLE)
writer = RouterDecisionWriter(conn)
for index in range(count):
writer.record_decision(
{
"decision_id": f"c{index}",
"session_key": "agent:calib:main",
"turn_index": index,
"proposed_tier": "c2",
"final_tier": "c1",
"source": "v4_phase3",
"trail": [{"stage": "confidence_gate", "applied": True}],
}
)
writer.close()
def _env(monkeypatch: Any, tmp_path: Path, db: Path | None) -> None:
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "home"))
if db is not None:
monkeypatch.setenv("OPENSQUILLA_ROUTER_DECISIONS_DB", str(db))
else:
monkeypatch.delenv("OPENSQUILLA_ROUTER_DECISIONS_DB", raising=False)
def _output_file(tmp_path: Path) -> Path:
return tmp_path / "home" / "state" / "router_calibration.json"
def test_calibrate_dry_run_prints_without_writing(tmp_path: Path, monkeypatch: Any) -> None:
db = tmp_path / "sessions.db"
_seed_db(db)
_env(monkeypatch, tmp_path, db)
result = runner.invoke(app, ["router", "calibrate", "--dry-run"])
assert result.exit_code == 0, result.output
assert "samples:" in result.output
assert "threshold_adjust:" in result.output
assert "dry-run" in result.output
assert not _output_file(tmp_path).exists()
def test_calibrate_writes_file(tmp_path: Path, monkeypatch: Any) -> None:
db = tmp_path / "sessions.db"
_seed_db(db)
_env(monkeypatch, tmp_path, db)
result = runner.invoke(app, ["router", "calibrate"])
assert result.exit_code == 0, result.output
out = _output_file(tmp_path)
assert out.exists()
payload = json.loads(out.read_text(encoding="utf-8"))
assert payload["sample_count"] == 40
# 40 clean c2 downgrades -> bias c2 down (clamped), threshold up.
assert payload["per_class_bias"] == {"c2": -0.15}
assert payload["threshold_adjust"] > 0.0
def test_calibrate_json_output(tmp_path: Path, monkeypatch: Any) -> None:
db = tmp_path / "sessions.db"
_seed_db(db)
_env(monkeypatch, tmp_path, db)
result = runner.invoke(app, ["router", "calibrate", "--json"])
assert result.exit_code == 0, result.output
doc = json.loads(result.output)
assert doc["wrote"] is True
assert doc["calibration"]["sample_count"] == 40
def test_calibrate_missing_db_is_neutral_no_crash(tmp_path: Path, monkeypatch: Any) -> None:
_env(monkeypatch, tmp_path, tmp_path / "does-not-exist.db")
result = runner.invoke(app, ["router", "calibrate", "--dry-run"])
assert result.exit_code == 0, result.output
assert "samples: 0" in result.output
assert not _output_file(tmp_path).exists()
+132
View File
@@ -0,0 +1,132 @@
from __future__ import annotations
import json
import tomllib
from pathlib import Path
from typer.testing import CliRunner
from opensquilla.cli.main import app
from opensquilla.onboarding.config_store import load_config, persist_config
runner = CliRunner()
def _invoke(config_path: Path, *args: str):
return runner.invoke(app, ["sandbox", *args, "--config", str(config_path)])
def test_sandbox_status_reports_default_full_host_access(tmp_path: Path) -> None:
config_path = tmp_path / "config.toml"
result = runner.invoke(
app,
["sandbox", "status", "--config", str(config_path), "--json"],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["run_mode"] == "full"
assert payload["run_mode_label"] == "Full Host Access"
assert payload["execution_target"] == "host"
assert payload["posture"] == "full"
assert payload["sandbox"]["sandbox"] is False
assert payload["sandbox"]["security_grading"] is False
assert payload["permissions"]["default_mode"] == "full"
assert payload["restart_required"] is False
def test_sandbox_trust_persists_trusted_run_mode(tmp_path: Path) -> None:
config_path = tmp_path / "config.toml"
result = _invoke(config_path, "trust")
assert result.exit_code == 0, result.output
cfg = load_config(config_path)
assert cfg.sandbox.run_mode == "trusted"
assert cfg.sandbox.sandbox is True
assert cfg.sandbox.security_grading is True
assert cfg.sandbox.network_default == "proxy_allowlist"
assert cfg.permissions.default_mode == "off"
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
assert data["sandbox"]["run_mode"] == "trusted"
# Sparse persistence omits values equal to the built-in defaults
# (sandbox on, grading on, proxy allowlist, permissions off); the
# effective posture is asserted via load_config above. Any value that
# is written must match the trusted posture.
assert data["sandbox"].get("sandbox", True) is True
assert data["sandbox"].get("security_grading", True) is True
assert data["sandbox"].get("network_default", "proxy_allowlist") == "proxy_allowlist"
assert data.get("permissions", {}).get("default_mode", "off") == "off"
assert "restart" in result.output.lower()
def test_sandbox_trust_repairs_legacy_disabled_network_default(tmp_path: Path) -> None:
config_path = tmp_path / "config.toml"
full = _invoke(config_path, "full")
assert full.exit_code == 0, full.output
cfg = load_config(config_path)
cfg.sandbox.network_default = "none"
persist_config(cfg, path=config_path)
result = _invoke(config_path, "trust")
assert result.exit_code == 0, result.output
cfg = load_config(config_path)
assert cfg.sandbox.run_mode == "trusted"
assert cfg.sandbox.network_default == "proxy_allowlist"
def test_sandbox_bypass_fails_without_mutating_config(tmp_path: Path) -> None:
config_path = tmp_path / "config.toml"
before = _invoke(config_path, "on")
assert before.exit_code == 0, before.output
result = _invoke(config_path, "bypass")
assert result.exit_code != 0
assert "removed" in result.output.lower()
assert "sandbox trust" in result.output
cfg = load_config(config_path)
assert cfg.sandbox.run_mode == "trusted"
assert cfg.sandbox.sandbox is True
assert cfg.sandbox.security_grading is True
assert cfg.permissions.default_mode == "off"
def test_sandbox_full_and_on_are_reversible(tmp_path: Path) -> None:
config_path = tmp_path / "config.toml"
full = _invoke(config_path, "full")
assert full.exit_code == 0, full.output
cfg = load_config(config_path)
assert cfg.sandbox.run_mode == "full"
assert cfg.sandbox.sandbox is False
assert cfg.sandbox.security_grading is False
assert cfg.sandbox.network_default == "none"
assert cfg.permissions.default_mode == "full"
on = _invoke(config_path, "on")
assert on.exit_code == 0, on.output
cfg = load_config(config_path)
assert cfg.sandbox.run_mode == "trusted"
assert cfg.sandbox.sandbox is True
assert cfg.sandbox.security_grading is True
assert cfg.sandbox.network_default == "proxy_allowlist"
assert cfg.permissions.default_mode == "off"
def test_sandbox_reset_restores_full_host_access(tmp_path: Path) -> None:
config_path = tmp_path / "config.toml"
full = _invoke(config_path, "full")
reset = _invoke(config_path, "reset")
assert full.exit_code == 0, full.output
assert reset.exit_code == 0, reset.output
cfg = load_config(config_path)
assert cfg.sandbox.run_mode == "full"
assert cfg.sandbox.sandbox is False
assert cfg.sandbox.security_grading is False
assert cfg.sandbox.network_default == "none"
assert cfg.permissions.default_mode == "full"
+200
View File
@@ -0,0 +1,200 @@
"""CLI tests for `opensquilla search benchmark`."""
from __future__ import annotations
import json
from typing import cast
from typer.testing import CliRunner
import opensquilla.cli.search_cmd as search_cmd # type: ignore[import-untyped]
from opensquilla.cli.main import app # type: ignore[import-untyped]
from opensquilla.search.types import DEFAULT_SEARCH_MAX_RESULTS
runner = CliRunner()
def _benchmark_payload() -> dict[str, object]:
return {
"profile": "smoke",
"measurement_kind": "synthetic_smoke",
"live": False,
"v1": {
"p50_latency_ms": 10,
"p95_latency_ms": 20,
"success_at_k": 0.7,
"external_tool_calls_per_question": 2,
"avg_returned_chars": 500,
"duplicate_url_rate": 0.2,
"fetch_success_rate": 0.8,
"provider_fallback_count": 1,
},
"v2": {
"p50_latency_ms": 12,
"p95_latency_ms": 24,
"success_at_k": 0.9,
"external_tool_calls_per_question": 1,
"avg_returned_chars": 300,
"duplicate_url_rate": 0.0,
"fetch_success_rate": 1.0,
"provider_fallback_count": 0,
},
"delta": {
"success_at_k": 0.2,
"external_tool_calls_per_question": -1,
"avg_returned_chars": -200,
},
"cases": [
{"id": "official_docs", "query": "python release", "kind": "technical_docs"}
],
}
def _numeric_metric(metrics: dict[str, object], key: str) -> int | float:
value = metrics[key]
assert isinstance(value, int | float)
return value
def _assert_benchmark_metrics(payload: dict[str, object]) -> None:
assert set(payload) >= {"v1", "v2", "delta"}
for arm in ("v1", "v2"):
assert isinstance(payload[arm], dict)
metrics = cast(dict[str, object], payload[arm])
assert set(metrics) >= {
"p50_latency_ms",
"p95_latency_ms",
"success_at_k",
"external_tool_calls_per_question",
"avg_returned_chars",
"duplicate_url_rate",
"fetch_success_rate",
"provider_fallback_count",
}
assert isinstance(payload["delta"], dict)
delta = cast(dict[str, object], payload["delta"])
assert set(delta) >= {
"external_tool_calls_per_question",
"avg_returned_chars",
"success_at_k",
}
assert isinstance(payload.get("cases"), list)
assert payload["cases"]
for row in cast(list[dict[str, object]], payload["cases"]):
assert set(row) >= {"id", "query", "kind"}
v1 = cast(dict[str, object], payload["v1"])
v2 = cast(dict[str, object], payload["v2"])
assert _numeric_metric(v2, "external_tool_calls_per_question") < _numeric_metric(
v1, "external_tool_calls_per_question"
)
assert _numeric_metric(v2, "avg_returned_chars") < _numeric_metric(v1, "avg_returned_chars")
def test_search_benchmark_smoke_json_uses_real_synthetic_output():
result = runner.invoke(app, ["search", "benchmark", "--profile", "smoke", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["profile"] == "smoke"
assert payload["measurement_kind"] == "synthetic_smoke"
assert payload["live"] is False
_assert_benchmark_metrics(payload)
assert payload["query_count"] == len(payload["cases"])
def test_search_benchmark_passes_options_to_helper(monkeypatch):
seen: list[tuple[str, int, int, bool]] = []
def fake_run_search_benchmark(
profile: str,
fetch_top_k: int,
max_results: int,
live: bool = False,
) -> dict[str, object]:
seen.append((profile, fetch_top_k, max_results, live))
return _benchmark_payload()
monkeypatch.setattr(search_cmd, "_run_search_benchmark", fake_run_search_benchmark)
result = runner.invoke(app, ["search", "benchmark", "--profile", "smoke", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["profile"] == "smoke"
assert payload["measurement_kind"] == "synthetic_smoke"
assert payload["live"] is False
_assert_benchmark_metrics(payload)
assert payload["delta"]["external_tool_calls_per_question"] == -1
assert payload["delta"]["avg_returned_chars"] == -200
assert seen == [("smoke", 3, DEFAULT_SEARCH_MAX_RESULTS, False)]
def test_search_benchmark_live_json_rejects_without_env(monkeypatch):
monkeypatch.delenv("OPENSQUILLA_LIVE_SEARCH", raising=False)
result = runner.invoke(
app,
["search", "benchmark", "--profile", "smoke", "--live", "--json"],
)
assert result.exit_code == 2
payload = json.loads(result.stdout)
assert payload["ok"] is False
assert payload["error_kind"] == "invalid_request"
assert "live benchmark" in payload["error"]
def test_search_benchmark_live_json_rejects_even_with_env(monkeypatch):
monkeypatch.setenv("OPENSQUILLA_LIVE_SEARCH", "1")
result = runner.invoke(
app,
["search", "benchmark", "--profile", "smoke", "--live", "--json"],
)
assert result.exit_code == 2
payload = json.loads(result.stdout)
assert payload["ok"] is False
assert payload["error_kind"] == "invalid_request"
assert "live gate test suite" in payload["error"]
assert "measurement_kind" not in payload
assert "v1" not in payload
assert "v2" not in payload
def test_search_benchmark_rejects_zero_max_results_json():
result = runner.invoke(
app,
["search", "benchmark", "--profile", "smoke", "--max-results", "0", "--json"],
)
assert result.exit_code == 2
payload = json.loads(result.stdout)
assert payload["ok"] is False
assert payload["error_kind"] == "invalid_request"
assert "max_results" in payload["error"]
def test_search_benchmark_rejects_negative_fetch_top_k_json():
result = runner.invoke(
app,
["search", "benchmark", "--profile", "smoke", "--fetch-top-k", "-1", "--json"],
)
assert result.exit_code == 2
payload = json.loads(result.stdout)
assert payload["ok"] is False
assert payload["error_kind"] == "invalid_request"
assert "fetch_top_k" in payload["error"]
def test_search_benchmark_rejects_unknown_profile_json():
result = runner.invoke(
app,
["search", "benchmark", "--profile", "unknown", "--json"],
)
assert result.exit_code == 2
payload = json.loads(result.stdout)
assert payload["ok"] is False
assert payload["error_kind"] == "invalid_request"
+59
View File
@@ -0,0 +1,59 @@
"""CLI tests for `opensquilla search`."""
from __future__ import annotations
from typer.testing import CliRunner
from opensquilla.cli.main import app
runner = CliRunner()
def test_search_list_shows_runtime_providers():
result = runner.invoke(app, ["search", "list"])
assert result.exit_code == 0, result.stdout
assert "brave" in result.stdout
assert "duckduckgo" in result.stdout
def test_search_configure_writes_config(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
result = runner.invoke(
app,
[
"search",
"configure",
"brave",
"--api-key",
"brave-secret",
"--max-results",
"8",
],
)
assert result.exit_code == 0, result.stdout
text = target.read_text()
assert 'search_provider = "brave"' in text
assert 'search_api_key = "brave-secret"' in text
assert "brave-secret" not in result.stdout
def test_search_configure_warns_when_env_key_is_not_set(tmp_path, monkeypatch):
target = tmp_path / "c.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.delenv("BRAVE_SEARCH_API_KEY", raising=False)
result = runner.invoke(
app,
[
"search",
"configure",
"brave",
"--api-key-env",
"BRAVE_SEARCH_API_KEY",
],
)
assert result.exit_code == 0, result.stdout
assert "Warning" in result.stdout
assert "BRAVE_SEARCH_API_KEY" in result.stdout
+148
View File
@@ -0,0 +1,148 @@
"""CLI tests for `opensquilla search query` canonical web search mode."""
from __future__ import annotations
import json
from typer.testing import CliRunner
import opensquilla.cli.search_cmd as search_cmd
from opensquilla.cli.main import app
from opensquilla.search.types import SearchOptions
runner = CliRunner()
def test_search_query_web_search_options_use_local_search(monkeypatch):
seen_options: list[SearchOptions] = []
async def fake_run_canonical_web_search(
options: SearchOptions,
**kwargs: object,
) -> dict[str, object]:
seen_options.append(options)
assert "fetcher" in kwargs
return {
"ok": True,
"query": options.query,
"mode": options.mode,
"provider_attempts": [{"provider": "tavily", "status": "success"}],
"diagnostics": {"returned_chars": 120},
"results": [
{
"rank": 1,
"title": "Python release",
"domain": "python.org",
"url": "https://www.python.org/downloads/",
"provider": "tavily",
"fetched": False,
"excerpt": "Python release notes",
}
],
}
monkeypatch.setattr(
search_cmd,
"run_canonical_web_search",
fake_run_canonical_web_search,
)
result = runner.invoke(
app,
[
"search",
"query",
"python release",
"--mode",
"auto",
"--fetch-top-k",
"0",
"--max-results",
"5",
"--json",
],
)
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["query"] == "python release"
assert payload["mode"] == "auto"
assert payload["provider_attempts"] == [{"provider": "tavily", "status": "success"}]
assert payload["diagnostics"] == {"returned_chars": 120}
assert payload["results"][0]["title"] == "Python release"
assert seen_options == [
SearchOptions(query="python release", mode="auto", fetch_top_k=0, max_results=5)
]
def test_search_query_web_search_text_output_renders_results(monkeypatch):
async def fake_run_canonical_web_search(
options: SearchOptions,
**kwargs: object,
) -> dict[str, object]:
assert "fetcher" in kwargs
return {
"ok": True,
"query": options.query,
"mode": options.mode,
"provider_attempts": [],
"diagnostics": {},
"results": [
{
"rank": 1,
"title": "Python release notes",
"domain": "python.org",
"url": "https://www.python.org/downloads/",
"provider": "tavily",
"fetched": True,
"excerpt": "Python 3 release details.",
}
],
}
monkeypatch.setattr(
search_cmd,
"run_canonical_web_search",
fake_run_canonical_web_search,
)
result = runner.invoke(
app,
["search", "query", "python release", "--fetch-top-k", "0"],
)
assert result.exit_code == 0, result.stdout
assert "Python release notes" in result.stdout
assert "python.org" in result.stdout
assert "https://www.python.org/downloads/" in result.stdout
def test_search_query_web_search_json_failure_exits_nonzero(monkeypatch):
async def fake_run_canonical_web_search(
options: SearchOptions,
**kwargs: object,
) -> dict[str, object]:
assert "fetcher" in kwargs
return {
"ok": False,
"query": options.query,
"mode": options.mode,
"provider_attempts": [{"provider": "tavily", "status": "error"}],
"diagnostics": {},
"results": [],
"error": {"message": "network down"},
}
monkeypatch.setattr(
search_cmd,
"run_canonical_web_search",
fake_run_canonical_web_search,
)
result = runner.invoke(
app,
["search", "query", "python release", "--mode", "auto", "--json"],
)
assert result.exit_code == 1
assert json.loads(result.stdout)["error"]["message"] == "network down"
@@ -0,0 +1,69 @@
"""Tests for _model_alias and PromptState.label in session_state."""
from __future__ import annotations
from opensquilla.cli.repl.session_state import PromptState, _model_alias
def test_alias_none_returns_ellipsis() -> None:
assert _model_alias(None) == ""
def test_alias_empty_string_returns_ellipsis() -> None:
assert _model_alias("") == ""
def test_alias_with_slash_returns_last_segment() -> None:
# "deepseek-v4-flash-20260423" is 26 chars — under 28, no ellipsis
result = _model_alias("deepseek/deepseek-v4-flash-20260423")
assert result == "deepseek-v4-flash-20260423"
assert len(result) == 26
def test_alias_long_segment_is_middle_ellipsized() -> None:
# Build a 40-char tail segment
tail = "a" * 20 + "b" * 20 # 40 chars — over 28
full = f"provider/{tail}"
result = _model_alias(full)
assert result == tail[:12] + "" + tail[-12:]
assert len(result) == 25 # 12 + 1 + 12
def test_alias_exactly_28_chars_no_ellipsis() -> None:
seg = "x" * 28
result = _model_alias(f"provider/{seg}")
assert result == seg
assert "" not in result
def test_alias_29_chars_is_ellipsized() -> None:
seg = "x" * 29
result = _model_alias(f"provider/{seg}")
assert "" in result
assert len(result) == 25 # 12 + 1 + 12
def test_alias_no_slash_returns_whole_string() -> None:
assert _model_alias("gpt-5.1") == "gpt-5.1"
def test_alias_no_slash_long_string_is_ellipsized() -> None:
# 40-char string with no slash — treated as the whole segment
seg = "z" * 40
result = _model_alias(seg)
assert result == seg[:12] + "" + seg[-12:]
def test_prompt_state_label_uses_alias() -> None:
state = PromptState(model="openai/gpt-5.1", elevated=None)
assert state.label == "[gpt-5.1 normal] you ▸ "
def test_prompt_state_label_none_model() -> None:
state = PromptState(model=None, elevated=None)
assert state.label == "[… normal] you ▸ "
def test_prompt_state_label_elevated_mode() -> None:
state = PromptState(model="anthropic/claude-opus-4", elevated="bypass")
assert state.label == "[claude-opus-4 bypass] you ▸ "
+215
View File
@@ -0,0 +1,215 @@
"""CLI smoke tests for `opensquilla skills meta runs ...`."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from typer.testing import CliRunner
from opensquilla.cli.main import app as cli_app
from opensquilla.persistence.meta_run_writer import open_meta_run_writer
from opensquilla.persistence.migrator import apply_pending
from opensquilla.skills.meta.types import MetaPlan, MetaResult, MetaStep
MIGRATIONS_DIR = Path(__file__).resolve().parents[1].parent / "migrations"
@pytest.fixture
def runner() -> CliRunner:
return CliRunner()
@pytest.fixture
def seeded_db(tmp_path: Path, monkeypatch):
db = str(tmp_path / "test.db")
apply_pending(db, MIGRATIONS_DIR)
w = open_meta_run_writer(db)
plan_a = MetaPlan(
name="alpha-skill", triggers=("t",), priority=10,
steps=(MetaStep(id="s1", skill="x", kind="agent"),),
)
plan_b = MetaPlan(
name="beta-skill", triggers=("t",), priority=10,
steps=(MetaStep(id="s1", skill="y", kind="agent"),),
)
rid_ok = w.begin_run_sync(
meta_skill_name="alpha-skill", meta_plan=plan_a,
triggered_by="soft_meta_invoke", inputs={"user_message": "hi"},
session_key="sess-1", turn_id="turn-1",
)
w.begin_step_sync(
run_id=rid_ok, step=plan_a.steps[0], effective_skill="x",
rendered_inputs={"a": 1},
)
w.finish_step_sync(
run_id=rid_ok, step_id="s1", status="ok", output_text="alpha-out",
)
w.finish_run_sync(
run_id=rid_ok, status="ok",
result=MetaResult(ok=True, final_text="alpha-out"),
)
rid_fail = w.begin_run_sync(
meta_skill_name="beta-skill", meta_plan=plan_b,
triggered_by="hard_takeover", inputs={},
session_key=None, turn_id=None,
)
w.finish_run_sync(
run_id=rid_fail, status="failed",
result=MetaResult(ok=False, error="boom", failed_step_id="s1"),
)
w.close()
monkeypatch.setenv("OPENSQUILLA_META_RUNS_DB", db)
return {"db": db, "rid_ok": rid_ok, "rid_fail": rid_fail}
def test_runs_list(runner: CliRunner, seeded_db) -> None:
result = runner.invoke(cli_app, ["skills", "meta", "runs", "list", "--json"])
assert result.exit_code == 0, result.output
data = json.loads(result.output)
assert len(data) == 2
def test_runs_list_filter_status(runner: CliRunner, seeded_db) -> None:
result = runner.invoke(
cli_app,
["skills", "meta", "runs", "list", "--status", "failed", "--json"],
)
assert result.exit_code == 0
data = json.loads(result.output)
assert len(data) == 1
assert data[0]["status"] == "failed"
def test_runs_show(runner: CliRunner, seeded_db) -> None:
result = runner.invoke(
cli_app, ["skills", "meta", "runs", "show", seeded_db["rid_ok"], "--json"],
)
assert result.exit_code == 0
data = json.loads(result.output)
assert data["meta_skill_name"] == "alpha-skill"
assert data["status"] == "ok"
assert data["summary"]["step_count"] == 1
assert data["summary"]["usage"]["available"] is False
def test_runs_steps(runner: CliRunner, seeded_db) -> None:
result = runner.invoke(
cli_app, ["skills", "meta", "runs", "steps", seeded_db["rid_ok"], "--json"],
)
assert result.exit_code == 0
data = json.loads(result.output)
assert len(data) == 1
assert data[0]["step_id"] == "s1"
assert data[0]["status"] == "ok"
def test_runs_failures(runner: CliRunner, seeded_db) -> None:
result = runner.invoke(cli_app, ["skills", "meta", "runs", "failures", "--json"])
assert result.exit_code == 0
data = json.loads(result.output)
assert len(data) == 1
assert data[0]["status"] == "failed"
def test_runs_replay_dry_run(runner: CliRunner, seeded_db) -> None:
"""W8: --dry-run prints DAG in the spec'd format."""
result = runner.invoke(
cli_app,
["skills", "meta", "runs", "replay", seeded_db["rid_ok"], "--dry-run", "--json"],
)
assert result.exit_code == 0
data = json.loads(result.output)
assert data["meta_skill_name"] == "alpha-skill"
assert data["plan_source"] == "historical_snapshot"
assert len(data["steps"]) == 1
def test_runs_draft_json(runner: CliRunner, seeded_db) -> None:
result = runner.invoke(
cli_app, ["skills", "meta", "runs", "draft", seeded_db["rid_ok"], "--json"],
)
assert result.exit_code == 0, result.output
data = json.loads(result.output)
assert data["source_run"]["run_id"] == seeded_db["rid_ok"]
assert data["name"] == "alpha-skill-draft"
assert data["composition"]["steps"][0]["id"] == "s1"
assert data["trigger_candidates"]
def test_runs_show_bad_id(runner: CliRunner, seeded_db) -> None:
result = runner.invoke(cli_app, ["skills", "meta", "runs", "show", "BOGUS", "--json"])
assert result.exit_code != 0
def test_runs_list_empty(runner: CliRunner, tmp_path: Path, monkeypatch) -> None:
db = str(tmp_path / "empty.db")
apply_pending(db, MIGRATIONS_DIR)
monkeypatch.setenv("OPENSQUILLA_META_RUNS_DB", db)
result = runner.invoke(cli_app, ["skills", "meta", "runs", "list", "--json"])
assert result.exit_code == 0
assert json.loads(result.output) == []
def test_runs_list_missing_db_prints_friendly_notice(
runner: CliRunner, tmp_path: Path, monkeypatch,
) -> None:
"""Before first gateway boot there is no sessions.db: the CLI must not
traceback on a missing state dir and must not create a stray DB file."""
db = tmp_path / "state" / "sessions.db" # parent dir intentionally missing
monkeypatch.setenv("OPENSQUILLA_META_RUNS_DB", str(db))
result = runner.invoke(cli_app, ["skills", "meta", "runs", "list"])
assert result.exit_code == 0, result.output
assert "no meta-run history yet" in result.output
assert not db.exists()
assert not db.parent.exists()
def test_runs_list_missing_db_json_emits_empty_list(
runner: CliRunner, tmp_path: Path, monkeypatch,
) -> None:
db = tmp_path / "state" / "sessions.db"
monkeypatch.setenv("OPENSQUILLA_META_RUNS_DB", str(db))
result = runner.invoke(cli_app, ["skills", "meta", "runs", "list", "--json"])
assert result.exit_code == 0, result.output
assert json.loads(result.output) == []
assert not db.exists()
def test_runs_failures_missing_db_json_emits_empty_list(
runner: CliRunner, tmp_path: Path, monkeypatch,
) -> None:
db = tmp_path / "state" / "sessions.db"
monkeypatch.setenv("OPENSQUILLA_META_RUNS_DB", str(db))
result = runner.invoke(cli_app, ["skills", "meta", "runs", "failures", "--json"])
assert result.exit_code == 0, result.output
assert json.loads(result.output) == []
assert not db.exists()
def test_runs_cost_missing_db_json_emits_empty_summary_shape(
runner: CliRunner, tmp_path: Path, monkeypatch,
) -> None:
db = tmp_path / "state" / "sessions.db"
monkeypatch.setenv("OPENSQUILLA_META_RUNS_DB", str(db))
result = runner.invoke(cli_app, ["skills", "meta", "runs", "cost", "--json"])
assert result.exit_code == 0, result.output
data = json.loads(result.output)
assert data["aggregate"]["run_count"] == 0
assert data["runs"] == []
assert not db.exists()
def test_runs_show_missing_db_exits_not_found_without_creating_db(
runner: CliRunner, tmp_path: Path, monkeypatch,
) -> None:
db = tmp_path / "state" / "sessions.db"
monkeypatch.setenv("OPENSQUILLA_META_RUNS_DB", str(db))
result = runner.invoke(cli_app, ["skills", "meta", "runs", "show", "SOMEID"])
assert result.exit_code == 2
assert not db.exists()
assert not db.parent.exists()
+22
View File
@@ -0,0 +1,22 @@
from __future__ import annotations
import sys
from io import BytesIO, TextIOWrapper
import typer
from opensquilla.cli.stdio import configure_stdio_for_unicode
def test_configure_stdio_for_unicode_allows_typer_echo_on_gbk_stream(
monkeypatch,
) -> None:
raw = BytesIO()
stream = TextIOWrapper(raw, encoding="cp936", errors="strict")
monkeypatch.setattr(sys, "stdout", stream)
configure_stdio_for_unicode()
typer.echo("hello 🦐")
stream.flush()
assert raw.getvalue().decode("utf-8").strip() == "hello 🦐"
+182
View File
@@ -0,0 +1,182 @@
"""Tests for the /meta gateway slash command (list + run)."""
from __future__ import annotations
import io
from typing import Any
import pytest
from rich.console import Console
from opensquilla.cli.repl.session_state import ChatSessionState
from opensquilla.cli.repl.stream import TurnResult
class _FakeGatewayClient:
"""Records generic RPC ``call`` invocations and returns canned payloads."""
def __init__(self, responses: dict[str, Any] | None = None) -> None:
self.calls: list[tuple[str, dict[str, Any] | None]] = []
self.responses = responses or {}
async def call(self, method: str, params: dict | None = None) -> Any:
self.calls.append((method, params))
return self.responses.get(method, {})
def _make_console() -> tuple[Console, io.StringIO]:
buffer = io.StringIO()
console = Console(file=buffer, force_terminal=False, width=100, highlight=False, no_color=True)
return console, buffer
@pytest.mark.asyncio
async def test_meta_no_arg_lists_skills(monkeypatch: pytest.MonkeyPatch) -> None:
from opensquilla.cli.repl import slash_adapter
from opensquilla.cli.repl.slash_adapter import (
GatewaySlashContext,
handle_gateway_slash_command,
)
console, buffer = _make_console()
monkeypatch.setattr(slash_adapter, "console", console)
client = _FakeGatewayClient(
responses={
"meta.list": {
"skills": [
{"name": "meta-tiny", "description": "A tiny meta-skill."},
{"name": "meta-plan", "description": "Plan things."},
]
}
}
)
state = ChatSessionState(session_key="agent:main:test", model="local/test")
handled = await handle_gateway_slash_command(
"/meta",
GatewaySlashContext(state=state, client=client, elevated_state={"mode": None}),
)
assert handled is True
assert client.calls == [("meta.list", {})]
rendered = buffer.getvalue()
assert "meta-tiny" in rendered
assert "meta-plan" in rendered
@pytest.mark.asyncio
async def test_meta_no_arg_disabled_prints_notice(monkeypatch: pytest.MonkeyPatch) -> None:
from opensquilla.cli.repl import slash_adapter
from opensquilla.cli.repl.slash_adapter import (
GatewaySlashContext,
handle_gateway_slash_command,
)
console, buffer = _make_console()
monkeypatch.setattr(slash_adapter, "console", console)
client = _FakeGatewayClient(responses={"meta.list": {"disabled": True}})
state = ChatSessionState(session_key="agent:main:test", model="local/test")
handled = await handle_gateway_slash_command(
"/meta",
GatewaySlashContext(state=state, client=client, elevated_state={"mode": None}),
)
assert handled is True
assert client.calls == [("meta.list", {})]
assert "disabled" in buffer.getvalue().lower()
@pytest.mark.asyncio
async def test_meta_run_ok_triggers_turn(monkeypatch: pytest.MonkeyPatch) -> None:
from opensquilla.cli.repl import slash_adapter
from opensquilla.cli.repl.slash_adapter import (
GatewaySlashContext,
handle_gateway_slash_command,
)
console, _buffer = _make_console()
monkeypatch.setattr(slash_adapter, "console", console)
captured: dict[str, Any] = {}
async def fake_stream_response_gateway(
gateway_client: object,
session_key: str,
message: str,
elevated_state: dict[str, str | None],
attachments: list[dict[str, Any]] | None = None,
*,
tui_output: object | None = None,
) -> TurnResult:
captured.update(
{
"client": gateway_client,
"session_key": session_key,
"message": message,
"elevated_state": elevated_state,
}
)
return TurnResult(text="launched")
monkeypatch.setattr(slash_adapter, "stream_response_gateway", fake_stream_response_gateway)
client = _FakeGatewayClient(responses={"meta.run": {"ok": True}})
state = ChatSessionState(session_key="agent:main:test", model="local/test")
handled = await handle_gateway_slash_command(
"/meta meta-tiny",
GatewaySlashContext(state=state, client=client, elevated_state={"mode": "on"}),
)
assert handled is True
assert client.calls == [
("meta.run", {"name": "meta-tiny", "sessionKey": "agent:main:test"}),
]
# A turn was triggered through the same path /image uses.
assert captured["session_key"] == "agent:main:test"
assert captured["message"] == "/meta meta-tiny"
assert captured["elevated_state"] == {"mode": "on"}
assert state.transcript.to_markdown()
@pytest.mark.asyncio
async def test_meta_run_not_ok_prints_error_and_skips_turn(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.cli.repl import slash_adapter
from opensquilla.cli.repl.slash_adapter import (
GatewaySlashContext,
handle_gateway_slash_command,
)
console, buffer = _make_console()
monkeypatch.setattr(slash_adapter, "console", console)
stream_calls: list[str] = []
async def fake_stream_response_gateway(*args: Any, **kwargs: Any) -> TurnResult:
stream_calls.append("called")
return TurnResult(text="should-not-happen")
monkeypatch.setattr(slash_adapter, "stream_response_gateway", fake_stream_response_gateway)
client = _FakeGatewayClient(
responses={"meta.run": {"ok": False, "error": "unknown meta-skill: bad"}}
)
state = ChatSessionState(session_key="agent:main:test", model="local/test")
handled = await handle_gateway_slash_command(
"/meta bad",
GatewaySlashContext(state=state, client=client, elevated_state={"mode": None}),
)
assert handled is True
assert client.calls == [
("meta.run", {"name": "bad", "sessionKey": "agent:main:test"}),
]
# No turn fired.
assert stream_calls == []
assert "unknown meta-skill: bad" in buffer.getvalue()
+210
View File
@@ -0,0 +1,210 @@
"""CLI surface for `opensquilla uninstall` — flags, guards, confirmation."""
from __future__ import annotations
import json
import os
import subprocess
import sys
from pathlib import Path
import pytest
from typer.testing import CliRunner
from opensquilla.cli import codetask_cmd
from opensquilla.cli.main import app
from opensquilla.uninstall import actions as actions_module
from opensquilla.uninstall import inventory as inventory_module
from opensquilla.uninstall.actions import ActionResult, ExecutionResult
from opensquilla.uninstall.inventory import DataBucket, Inventory
runner = CliRunner()
def _fake_inventory(home: Path) -> Inventory:
state = home / "state"
state.mkdir(parents=True, exist_ok=True)
(home / "config.toml").write_text("x")
return Inventory(
method="pip",
home=home,
state_root=state,
config_path=None,
entrypoints=[],
program_paths=[],
package_uninstall=["pip", "uninstall", "-y", "opensquilla"],
buckets=[
DataBucket("config.toml", home / "config.toml", "config", "config"),
DataBucket("state directory", state, "user-data", "state"),
],
services=[],
receipt=None,
notes=[],
home_recognized=True,
)
def _patch_discover(monkeypatch, home: Path) -> None:
monkeypatch.setattr(inventory_module, "discover", lambda: _fake_inventory(home))
def test_dry_run_json_emits_plan_and_does_nothing(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
def _boom(*_a, **_k):
raise AssertionError("execute must not run during --dry-run")
monkeypatch.setattr(actions_module, "execute", _boom)
result = runner.invoke(app, ["uninstall", "--dry-run", "--json"])
assert result.exit_code == 0, result.stdout
payload = json.loads(result.stdout)
assert payload["dry_run"] is True
assert payload["plan"]["method"] == "pip"
def test_dry_run_json_subprocess_stdout_stays_machine_readable(tmp_path: Path) -> None:
"""Import-time env loading must not pollute stdout before JSON output."""
home = tmp_path / "home"
env_home = home / ".opensquilla"
env_home.mkdir(parents=True)
(env_home / ".env").write_text("OPENSQUILLA_ENV_JSON_TEST=1\n", encoding="utf-8")
env = os.environ.copy()
env.update(
{
"HOME": str(home),
"USERPROFILE": str(home),
"HTTP_PROXY": "http://127.0.0.1:9",
}
)
env.pop("OPENSQUILLA_TRUST_ENV", None)
completed = subprocess.run(
[sys.executable, "-m", "opensquilla.cli.main", "uninstall", "--dry-run", "--json"],
cwd=Path(__file__).resolve().parents[2],
env=env,
capture_output=True,
text=True,
timeout=30,
check=False,
)
assert completed.returncode == 0, completed.stderr + completed.stdout
assert completed.stdout.lstrip().startswith("{")
payload = json.loads(completed.stdout)
assert payload["dry_run"] is True
def test_non_interactive_without_yes_refuses(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
def _boom(*_a, **_k):
raise AssertionError("execute must not run without confirmation")
monkeypatch.setattr(actions_module, "execute", _boom)
result = runner.invoke(app, ["uninstall", "--json"])
assert result.exit_code == 2, result.stdout
assert "CONFIRMATION_REQUIRED" in (result.stdout + (result.stderr or ""))
def test_yes_json_executes(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
captured = {}
def _fake_execute(plan, inventory, **_kwargs):
captured["ran"] = True
return ExecutionResult(
results=[ActionResult("run-package-uninstall", "ok", ok=True)], ok=True
)
monkeypatch.setattr(actions_module, "execute", _fake_execute)
result = runner.invoke(app, ["uninstall", "--yes", "--json"])
assert result.exit_code == 0, result.stdout
assert captured.get("ran") is True
payload = json.loads(result.stdout)
assert payload["ok"] is True
@pytest.mark.parametrize("purge_flag", ["--purge-state", "--purge-config", "--purge-all"])
def test_desktop_profile_routes_data_deletion_to_complete_desktop_cleanup(
monkeypatch,
tmp_path: Path,
purge_flag: str,
) -> None:
monkeypatch.setenv("OPENSQUILLA_PROFILE_KIND", "desktop-primary")
def _boom(*_args, **_kwargs):
raise AssertionError("generic Desktop purge must not inspect or delete partial data")
monkeypatch.setattr(inventory_module, "discover", _boom)
monkeypatch.setattr(actions_module, "execute", _boom)
result = runner.invoke(app, ["uninstall", purge_flag, "--yes", "--json"])
assert result.exit_code == 2, result.stdout
assert "DESKTOP_CLEANUP_REQUIRED" in (result.stdout + (result.stderr or ""))
def test_purge_all_requires_confirmation_phrase(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
monkeypatch.setattr(codetask_cmd, "_stdin_is_tty", lambda: True) # allow interactive path
def _boom(*_a, **_k):
raise AssertionError("execute must not run on a mismatched phrase")
monkeypatch.setattr(actions_module, "execute", _boom)
result = runner.invoke(app, ["uninstall", "--purge-all"], input="nope\n")
assert result.exit_code == 2, result.stdout
assert "requires confirmation" in (result.stdout + (result.stderr or ""))
def test_yes_purge_all_without_phrase_is_refused(monkeypatch, tmp_path: Path) -> None:
"""`--yes --purge-all` must NOT wipe without the explicit second-factor phrase."""
_patch_discover(monkeypatch, tmp_path / "home")
def _boom(*_a, **_k):
raise AssertionError("execute must not run without the purge-all phrase")
monkeypatch.setattr(actions_module, "execute", _boom)
result = runner.invoke(app, ["uninstall", "--yes", "--purge-all", "--json"])
assert result.exit_code == 2, result.stdout
assert "CONFIRMATION_REQUIRED" in (result.stdout + (result.stderr or ""))
def test_yes_purge_all_with_phrase_executes(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
captured = {}
def _fake_execute(plan, inventory, **_kwargs):
captured["ran"] = True
return ExecutionResult(results=[], ok=True)
monkeypatch.setattr(actions_module, "execute", _fake_execute)
result = runner.invoke(
app,
["uninstall", "--yes", "--purge-all", "--json", "--confirm-purge-all", "delete everything"],
)
assert result.exit_code == 0, result.stdout
assert captured.get("ran") is True
def test_purge_all_proceeds_on_correct_phrase(monkeypatch, tmp_path: Path) -> None:
_patch_discover(monkeypatch, tmp_path / "home")
monkeypatch.setattr(codetask_cmd, "_stdin_is_tty", lambda: True)
captured = {}
def _fake_execute(plan, inventory, **_kwargs):
captured["ran"] = True
return ExecutionResult(results=[], ok=True)
monkeypatch.setattr(actions_module, "execute", _fake_execute)
result = runner.invoke(app, ["uninstall", "--purge-all"], input="delete everything\n")
assert result.exit_code == 0, result.stdout
assert captured.get("ran") is True
+14
View File
@@ -0,0 +1,14 @@
from __future__ import annotations
from opensquilla.cli.url_utils import normalize_gateway_url
def test_normalize_gateway_url_preserves_query_and_fragment() -> None:
assert (
normalize_gateway_url("https://gateway.example.com/ws?token=abc#trace")
== "wss://gateway.example.com/ws?token=abc#trace"
)
def test_normalize_gateway_url_adds_ws_path_without_dropping_query() -> None:
assert normalize_gateway_url("gateway.example.com?token=abc") == "ws://gateway.example.com/ws?token=abc"