mirror of
https://github.com/TencentCloud/Octop.git
synced 2026-10-20 06:19:17 +08:00
* feat(chat): subagent mention picker, skill slash insertion, ask_agent thread - Chat @ menu adds a subagent picker and mention-based peer injection - skill button inserts a leading `/slug` via the slash menu instead of the skills whitelist; @ menu no longer lists skills - ask_agent leaves a normal conversation in the target expert using the caller session_key and a stable thread_id~peer id, without hijacking the peer's bound session or routing through the gateway inbox - workspace manifest.json guidance cards are written to harness metadata on agent start and re-read on list_peers/agent_list/@ injection - octop service start/restart sets LimitNOFILE via a systemd drop-in Note: the pre-commit gate is bypassed because this sandbox's installed harness-agent (orcakit-harness-agent 1.0.6) lacks HarnessAgent.aappend_messages, which the project's actual runtime provides (already used by infra/cron/delivery.py on develop). Verified locally: ruff, mypy (against an API-complete harness-agent checkout), dashboard build, and pytest -m "not live" (3068 passed; the single failure is a harness-agent-version artifact in test_build_harness_config_defaults_local_shell_backend, unrelated to this change). * fix(providers): carry x-opencode-session header for OpenCode Go gateway OpenCode Go (https://opencode.ai/zen/go) rejects chat requests without x-opencode-session (HTTP 400 MissingSessionID). Store sets ProviderConfig session_header for Go URLs so harness fills it with the live thread_id; probes and model listing inject a throwaway UUID since they never enter HarnessAgent. User-configured headers win. Requires orcakit-harness-agent>=1.0.7. * fix(harness): pass log_dir to HarnessAgentManager to keep diagnostics out of ~/.harness-agent/logs HarnessAgentConfig.log_dir is a discarded InitVar in 1.0.7, so route the process log directory through HarnessAgentManager(log_dir=...) at boot. * fix(setup): guard RLIMIT_NOFILE read on POSIX for mypy + Windows resource.getrlimit/RLIMIT_NOFILE are POSIX-only; short-circuit on sys.platform == 'win32' so mypy narrows the module and Windows runtimes skip the call. --------- Co-authored-by: jubaoliang <jubaoliang@tencent.com>
249 lines
8.5 KiB
Python
249 lines
8.5 KiB
Python
"""Tests for `octop service`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import getpass
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from octop.cli.main import cli
|
|
from octop.infra.setup.service import (
|
|
ServiceRuntime,
|
|
ServiceStatus,
|
|
launchd_domain,
|
|
unit_path,
|
|
)
|
|
|
|
pytestmark = pytest.mark.skipif(os.name != "posix", reason="POSIX service management")
|
|
|
|
|
|
def _runtime(tmp_path: Path, *, mode: str = "systemd", scope: str = "system") -> ServiceRuntime:
|
|
octop_bin = tmp_path / "bin" / "octop"
|
|
octop_bin.parent.mkdir(parents=True)
|
|
octop_bin.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
return ServiceRuntime(
|
|
mode=mode, # type: ignore[arg-type]
|
|
host="127.0.0.1",
|
|
port=8088,
|
|
home=tmp_path,
|
|
octop_bin=octop_bin,
|
|
run_as_user=getpass.getuser(),
|
|
scope=scope, # type: ignore[arg-type]
|
|
)
|
|
|
|
|
|
def test_service_help_lists_commands() -> None:
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["service", "--help"])
|
|
assert result.exit_code == 0
|
|
for cmd in ("start", "stop", "restart", "status"):
|
|
assert cmd in result.output
|
|
|
|
|
|
def test_service_start_installs_and_starts(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
import octop.cli.commands.service as service_cmd
|
|
|
|
runtime = _runtime(tmp_path)
|
|
calls: list[str] = []
|
|
|
|
monkeypatch.setattr(service_cmd, "build_runtime", lambda **kw: runtime)
|
|
monkeypatch.setattr(
|
|
service_cmd,
|
|
"install_service",
|
|
lambda rt, force=False: calls.append(f"install:{force}"),
|
|
)
|
|
monkeypatch.setattr(
|
|
service_cmd, "start_service", lambda rt, apply_unit=False: calls.append("start")
|
|
)
|
|
monkeypatch.setattr(
|
|
service_cmd,
|
|
"collect_service_status",
|
|
lambda rt, check_health=True, health_retries=1, health_delay_seconds=1.5: ServiceStatus(
|
|
mode="systemd",
|
|
installed=True,
|
|
active=True,
|
|
enabled=True,
|
|
detail="active",
|
|
health_ok=True,
|
|
health_detail="{'ok': true}",
|
|
),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["service", "start"])
|
|
assert result.exit_code == 0, result.output
|
|
assert calls == ["install:False", "start"]
|
|
assert "active=True" in result.output
|
|
|
|
|
|
def test_service_start_applies_unit_when_rewritten(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""Rewriting the unit (e.g. LimitNOFILE) must bounce systemd so the new cap applies."""
|
|
import octop.cli.commands.service as service_cmd
|
|
|
|
runtime = _runtime(tmp_path)
|
|
applied: list[bool] = []
|
|
|
|
monkeypatch.setattr(service_cmd, "build_runtime", lambda **kw: runtime)
|
|
monkeypatch.setattr(service_cmd, "install_service", lambda rt, force=False: True)
|
|
monkeypatch.setattr(
|
|
service_cmd, "start_service", lambda rt, apply_unit=False: applied.append(apply_unit)
|
|
)
|
|
monkeypatch.setattr(
|
|
service_cmd,
|
|
"collect_service_status",
|
|
lambda rt, check_health=True, health_retries=1, health_delay_seconds=1.5: ServiceStatus(
|
|
mode="systemd",
|
|
installed=True,
|
|
active=True,
|
|
enabled=True,
|
|
detail="active",
|
|
health_ok=True,
|
|
health_detail="{'ok': true}",
|
|
),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["service", "start"])
|
|
assert result.exit_code == 0, result.output
|
|
assert applied == [True]
|
|
|
|
|
|
def test_service_start_emits_diagnostic_hint_when_health_unreachable(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""On health-check failure, surface a hint pointing to the service logs so
|
|
users don't have to dig through launchd/systemd docs."""
|
|
import octop.cli.commands.service as service_cmd
|
|
|
|
runtime = _runtime(tmp_path)
|
|
monkeypatch.setattr(service_cmd, "build_runtime", lambda **kw: runtime)
|
|
monkeypatch.setattr(service_cmd, "install_service", lambda rt, force=False: None)
|
|
monkeypatch.setattr(service_cmd, "start_service", lambda rt, apply_unit=False: None)
|
|
monkeypatch.setattr(
|
|
service_cmd,
|
|
"collect_service_status",
|
|
lambda rt, check_health=True, health_retries=1, health_delay_seconds=1.5: ServiceStatus(
|
|
mode="systemd",
|
|
installed=True,
|
|
active=True,
|
|
enabled=True,
|
|
detail="active",
|
|
health_ok=False,
|
|
health_detail="connection refused",
|
|
),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["service", "start"])
|
|
# `start` is warn-only, so exit code stays 0 but the hint must appear.
|
|
assert result.exit_code == 0, result.output
|
|
assert "connection refused" in result.output
|
|
assert "journalctl -u octop" in result.output
|
|
|
|
|
|
def test_service_status_exits_when_health_unreachable(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
import octop.cli.commands.service as service_cmd
|
|
|
|
runtime = _runtime(tmp_path)
|
|
monkeypatch.setattr(service_cmd, "build_runtime", lambda **kw: runtime)
|
|
monkeypatch.setattr(
|
|
service_cmd,
|
|
"collect_service_status",
|
|
lambda rt, check_health=True, health_retries=1, health_delay_seconds=1.5: ServiceStatus(
|
|
mode="systemd",
|
|
installed=True,
|
|
active=True,
|
|
enabled=True,
|
|
detail="active",
|
|
health_ok=False,
|
|
health_detail="connection refused",
|
|
),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["service", "status"])
|
|
assert result.exit_code == 1
|
|
assert "connection refused" in result.output
|
|
|
|
|
|
def test_service_start_forwards_user_scope(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
"""`--scope user` must reach build_runtime so the user-domain path is used."""
|
|
import octop.cli.commands.service as service_cmd
|
|
|
|
runtime = _runtime(tmp_path, mode="launchd", scope="user")
|
|
captured_kwargs: dict[str, object] = {}
|
|
|
|
def _fake_build_runtime(**kwargs: object) -> ServiceRuntime:
|
|
captured_kwargs.update(kwargs)
|
|
return runtime
|
|
|
|
monkeypatch.setattr(service_cmd, "build_runtime", _fake_build_runtime)
|
|
monkeypatch.setattr(service_cmd, "install_service", lambda rt, force=False: None)
|
|
monkeypatch.setattr(service_cmd, "start_service", lambda rt, apply_unit=False: None)
|
|
monkeypatch.setattr(
|
|
service_cmd,
|
|
"collect_service_status",
|
|
lambda rt, check_health=True, health_retries=1, health_delay_seconds=1.5: ServiceStatus(
|
|
mode="launchd",
|
|
installed=True,
|
|
active=True,
|
|
enabled=True,
|
|
detail="running",
|
|
health_ok=True,
|
|
health_detail="{'ok': true}",
|
|
),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["service", "start", "--scope", "user"])
|
|
assert result.exit_code == 0, result.output
|
|
assert captured_kwargs.get("scope") == "user"
|
|
# And the output must tell the user where the plist lives.
|
|
assert str(unit_path("launchd", scope="user", run_as_user=runtime.run_as_user)) in result.output
|
|
assert launchd_domain("user") in result.output
|
|
|
|
|
|
def test_service_status_shows_plist_and_domain(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""`status` must report plist path and launchd domain so users can see
|
|
which scope is being inspected."""
|
|
import octop.cli.commands.service as service_cmd
|
|
|
|
runtime = _runtime(tmp_path, mode="launchd", scope="user")
|
|
monkeypatch.setattr(service_cmd, "build_runtime", lambda **kw: runtime)
|
|
monkeypatch.setattr(
|
|
service_cmd,
|
|
"collect_service_status",
|
|
lambda rt, check_health=True, health_retries=1, health_delay_seconds=1.5: ServiceStatus(
|
|
mode="launchd",
|
|
installed=True,
|
|
active=True,
|
|
enabled=True,
|
|
detail="running",
|
|
health_ok=True,
|
|
health_detail="{'ok': true}",
|
|
),
|
|
)
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["service", "status", "--scope", "user"])
|
|
assert result.exit_code == 0, result.output
|
|
assert str(unit_path("launchd", scope="user", run_as_user=runtime.run_as_user)) in result.output
|
|
assert launchd_domain("user") in result.output
|
|
|
|
|
|
def test_service_status_rejects_garbage_scope() -> None:
|
|
"""Click-level validation — unknown scopes never reach build_runtime."""
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["service", "status", "--scope", "nonsense"])
|
|
assert result.exit_code != 0
|
|
assert "scope" in result.output.lower()
|