完善 agent/workspace 运行链路与工具结果治理测试覆盖,确保路由、标题生成与上下文截断行为稳定可回归。

同时将 data/wiki 用户数据纳入忽略规则,避免运行态数据被误纳入版本管理。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-26 15:55:46 +08:00
parent 45e412b4ab
commit 20362527e2
17 changed files with 974 additions and 41 deletions

View file

@ -127,6 +127,28 @@ class AdminChatStreamAsyncTaskTests(unittest.TestCase):
self.assertEqual(str(body.get("mode") or ""), "async_task")
self.assertTrue(str(body.get("task_id") or "").strip())
def test_async_task_first_turn_renames_new_chat_title(self) -> None:
token = self._login()
headers = {
"authorization": f"Bearer {token}",
"accept": "application/json",
"content-type": "application/json",
}
created = self.client.post("/admin/api/chat/sessions", headers=headers, json={}).json()
sid = str(((created.get("session") or {}).get("id")) or "")
self.assertTrue(sid)
send = self.client.post(
f"/admin/api/chat/sessions/{sid}/messages",
headers=headers,
json={"text": "请总结并发送到项目群"},
)
self.assertEqual(send.status_code, 200)
self.assertTrue(send.json().get("ok"), send.json())
sess = self.store.get_session(sid)
self.assertIsNotNone(sess)
title = str(getattr(sess, "title", "") or "")
self.assertNotIn(title, {"新会话", "New Chat"})
def test_async_task_payload_contains_selected_specialist(self) -> None:
token = self._login()
headers = {
@ -209,6 +231,71 @@ class AdminChatStreamAsyncTaskTests(unittest.TestCase):
self.assertEqual(str(payload.get("requested_specialist") or ""), "ops")
self.assertEqual(str(payload.get("memory_mode") or ""), "store_only")
def test_new_session_inherits_user_mode_preference(self) -> None:
token = self._login()
headers = {
"authorization": f"Bearer {token}",
"accept": "application/json",
"content-type": "application/json",
}
set_resp = self.client.post(
f"/admin/api/chat/sessions/{self.session_id}/mode",
headers=headers,
json={"interaction_mode": "expert", "specialist": "ops", "memory_mode": "store_only"},
)
self.assertEqual(set_resp.status_code, 200)
create_resp = self.client.post(
"/admin/api/chat/sessions",
headers=headers,
json={"title": "new one"},
)
self.assertEqual(create_resp.status_code, 200)
created = create_resp.json()
self.assertTrue(created.get("ok"), created)
new_session_id = str(((created.get("session") or {}).get("id")) or "")
self.assertTrue(new_session_id)
mode_resp = self.client.get(
f"/admin/api/chat/sessions/{new_session_id}/mode",
headers={"authorization": f"Bearer {token}", "accept": "application/json"},
)
self.assertEqual(mode_resp.status_code, 200)
mode_body = mode_resp.json()
self.assertTrue(mode_body.get("ok"), mode_body)
self.assertEqual(str(mode_body.get("interaction_mode") or ""), "expert")
self.assertEqual(str(mode_body.get("specialist") or ""), "ops")
self.assertEqual(str(mode_body.get("memory_mode") or ""), "store_only")
def test_new_session_default_mode_is_expert_generalist(self) -> None:
token = self._login()
headers = {
"authorization": f"Bearer {token}",
"accept": "application/json",
"content-type": "application/json",
}
create_resp = self.client.post(
"/admin/api/chat/sessions",
headers=headers,
json={"title": "default mode chat"},
)
self.assertEqual(create_resp.status_code, 200)
created = create_resp.json()
self.assertTrue(created.get("ok"), created)
new_session_id = str(((created.get("session") or {}).get("id")) or "")
self.assertTrue(new_session_id)
mode_resp = self.client.get(
f"/admin/api/chat/sessions/{new_session_id}/mode",
headers={"authorization": f"Bearer {token}", "accept": "application/json"},
)
self.assertEqual(mode_resp.status_code, 200)
mode_body = mode_resp.json()
self.assertTrue(mode_body.get("ok"), mode_body)
self.assertEqual(str(mode_body.get("interaction_mode") or ""), "expert")
self.assertEqual(str(mode_body.get("specialist") or ""), "generalist")
self.assertEqual(str(mode_body.get("memory_mode") or ""), "default")
def test_admin_dynamic_expert_stats_endpoint(self) -> None:
token = self._login()
headers = {
@ -322,6 +409,13 @@ class AdminChatStreamAsyncTaskTests(unittest.TestCase):
self.assertTrue(get_body.get("ok"), get_body)
self.assertEqual(int((get_body.get("limits") or {}).get("max_rows_read") or 0), 5000)
self.assertEqual(int((get_body.get("limits") or {}).get("sql_timeout_ms") or 0), 8000)
# newly added replay caps should exist with sane defaults
self.assertGreaterEqual(int((get_body.get("limits") or {}).get("image_result_replay_cap_chars") or 0), 600)
self.assertGreaterEqual(int((get_body.get("limits") or {}).get("video_result_replay_cap_chars") or 0), 600)
self.assertGreaterEqual(int((get_body.get("limits") or {}).get("video_transcript_chunk_size") or 0), 1)
self.assertGreaterEqual(int((get_body.get("limits") or {}).get("video_transcript_chunk_overlap") or 0), 0)
self.assertGreaterEqual(int((get_body.get("limits") or {}).get("archive_max_depth") or 0), 1)
self.assertGreaterEqual(int((get_body.get("limits") or {}).get("archive_max_file_count") or 0), 1)
set_resp = self.client.post(
"/admin/api/chat/settings/attachment-limits",
@ -334,6 +428,12 @@ class AdminChatStreamAsyncTaskTests(unittest.TestCase):
"max_excel_sheets": 8,
"large_table_preview_rows": 33,
"sql_timeout_ms": 1234,
"video_transcript_chunk_size": 1700,
"video_transcript_chunk_overlap": 240,
"archive_max_depth": 3,
"archive_max_file_count": 555,
"archive_max_entry_bytes": 123456,
"archive_max_total_uncompressed_bytes": 654321,
}
},
)
@ -347,6 +447,12 @@ class AdminChatStreamAsyncTaskTests(unittest.TestCase):
self.assertEqual(int(limits.get("max_excel_sheets") or 0), 8)
self.assertEqual(int(limits.get("large_table_preview_rows") or 0), 33)
self.assertEqual(int(limits.get("sql_timeout_ms") or 0), 1234)
self.assertEqual(int(limits.get("video_transcript_chunk_size") or 0), 1700)
self.assertEqual(int(limits.get("video_transcript_chunk_overlap") or 0), 240)
self.assertEqual(int(limits.get("archive_max_depth") or 0), 3)
self.assertEqual(int(limits.get("archive_max_file_count") or 0), 555)
self.assertEqual(int(limits.get("archive_max_entry_bytes") or 0), 123456)
self.assertEqual(int(limits.get("archive_max_total_uncompressed_bytes") or 0), 654321)
if __name__ == "__main__":

View file

@ -0,0 +1,33 @@
from __future__ import annotations
from oclaw.runtime.agent_core_attempt import _should_after_turn_memory
from oclaw.runtime.types import StandardMessage
def _msg(interaction_mode: str, extra: dict | None = None) -> StandardMessage:
md = {"interaction_mode": interaction_mode}
if isinstance(extra, dict):
md.update(extra)
return StandardMessage(
session_id="s1",
tenant_id="t1",
user_id="u1",
role="member",
channel="admin_chat",
text="hello",
attachments=[],
metadata=md,
)
def test_after_turn_memory_disabled_for_comprehensive_mode() -> None:
assert _should_after_turn_memory(_msg("comprehensive")) is False
def test_after_turn_memory_disabled_for_expert_mode_by_default() -> None:
assert _should_after_turn_memory(_msg("expert")) is False
def test_after_turn_memory_cannot_be_forced_by_metadata() -> None:
assert _should_after_turn_memory(_msg("expert", {"enable_after_turn_memory": True})) is False

View file

@ -275,6 +275,65 @@ def test_gateway_comprehensive_mode_manager_first_selects_specialist(monkeypatch
assert chosen.get("sid") == "image"
assert captured.get("exec_text") == "Please edit the image background."
def test_gateway_comprehensive_mode_writes_task_assignment_reasoning(monkeypatch: pytest.MonkeyPatch) -> None:
written: list[dict] = []
class Store:
def get_setting(self, _k: str) -> str:
return ""
def add_trace_event(self, **_kwargs: object) -> None:
return None
def add_message(self, **kwargs: object) -> None:
written.append(dict(kwargs))
class _ManagerModel:
def chat(self, _messages, _tools, *, on_token=None):
return LLMResponse(
content='{"route":{"specialist":"ops","reason":"ops task"},"dispatch":{"instruction_text":"请检查并修复网关启动失败。"}}',
tool_calls=[],
)
class _Exec:
def __init__(self, model=None):
self.model = model
self.tools = object()
self.system_prompt = ""
monkeypatch.setattr(
"oclaw.runtime.gateway.get_manager_prompt_prebuild",
lambda **kwargs: {
"manager_context": "manager",
"allowed_fixed": ("generalist", "ops", "image", "memory"),
"allowed_fixed_quoted": '"generalist", "ops", "image", "memory"',
},
)
monkeypatch.setattr(
"oclaw.runtime.gateway.run_agent_core",
lambda **kwargs: SimpleNamespace(outcome=SimpleNamespace(final_text="specialist_answer")),
)
gw = OclawGateway(store=Store())
msg = StandardMessage(
session_id="sid-assign-1",
tenant_id="t1",
user_id="u1",
role="user",
channel="admin_chat",
text="网关起不来,帮我修复",
attachments=[],
metadata={"interaction_mode": "comprehensive"},
)
_ = gw.handle_turn(msg=msg, lang="zh", executor=_Exec(model=_ManagerModel()))
reasoning_rows = [x for x in written if str(x.get("event_type") or "") == "reasoning"]
assert reasoning_rows, "expected task-assignment reasoning row"
content = str(reasoning_rows[-1].get("content") or "")
assert "任务分配" in content
assert "specialist=ops" in content
assert "请检查并修复网关启动失败" in content
def test_gateway_comprehensive_mode_has_manager_final_pass(monkeypatch: pytest.MonkeyPatch) -> None:
class Store:
def get_setting(self, _k: str) -> str:

View file

@ -41,3 +41,40 @@ def test_oclaw_tool_result_context_guard_truncates_large_tool_message(tmp_path:
assert len(guarded) <= _OCLAW_TOOL_RESULT_HARD_CAP_CHARS + 2000
assert "_tool_result_guarded" in guarded
def test_oclaw_tool_result_context_guard_skips_active_turn_tool_messages(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "ops.sqlite"))
sess = store.create_session("t")
turn_uuid = "turn-active-1"
store.add_message(
session_id=sess.id,
role="assistant",
content="",
tool_calls=[{"id": "c1", "name": "echo", "arguments": {"x": 1}}],
turn_uuid=turn_uuid,
)
huge = "X" * (_OCLAW_TOOL_RESULT_HARD_CAP_CHARS + 5000)
store.add_message(
session_id=sess.id,
role="tool",
content='{"ok": true, "blob": "' + huge + '"}',
tool_calls={"tool_call_id": "c1", "name": "echo", "assistant_message_id": 1},
turn_uuid=turn_uuid,
)
msgs = _build_model_context(
store=store,
session_id=sess.id,
max_messages=50,
system_prompt="sys",
model=RuleBasedChatModel(),
lang="zh",
memory_context=None,
trace_id="t1",
parent_span_id=None,
active_turn_uuid=turn_uuid,
)
tool_msgs = [m for m in msgs if m.get("role") == "tool"]
assert tool_msgs, msgs
raw = str(tool_msgs[-1].get("content") or "")
assert "_tool_result_guarded" not in raw

View file

@ -0,0 +1,59 @@
from __future__ import annotations
import hashlib
from pathlib import Path
from oclaw.platform.persistence.sqlite_store import SqliteStore
from oclaw.runtime.types import StandardMessage
from oclaw.runtime.worker import _maybe_generate_title_on_third_round
class _DummyResp:
def __init__(self, content: str) -> None:
self.content = content
class _DummyModel:
def chat(self, _messages, _tools, on_token=None): # noqa: ANN001
return _DummyResp("第三轮标题")
def test_worker_third_round_title_generation_updates_stage3(tmp_path: Path) -> None:
store = SqliteStore(str(tmp_path / "worker-title.sqlite"))
tenant = store.create_tenant("Team")
user = store.create_user_account(
tenant_id=str(tenant["id"]),
username="tester",
display_name="Tester",
role="owner",
password_hash=hashlib.sha256("test-pass".encode("utf-8")).hexdigest(),
is_active=True,
)
session = store.create_session_for_user(
title="第一轮标题",
tenant_id=str(tenant["id"]),
user_id=str(user["id"]),
)
sid = str(session.id)
store.set_setting(f"AIA_SESSION_AUTO_TITLE_STAGE:{sid}", "1")
store.add_message(session_id=sid, role="user", content="第一轮问题")
store.add_message(session_id=sid, role="assistant", content="第一轮回答")
store.add_message(session_id=sid, role="user", content="第二轮问题")
store.add_message(session_id=sid, role="assistant", content="第二轮回答")
msg = StandardMessage(
session_id=sid,
tenant_id=str(tenant["id"]),
user_id=str(user["id"]),
role="member",
channel="admin_chat",
text="第三轮问题",
attachments=[],
metadata={"lang": "zh"},
)
_maybe_generate_title_on_third_round(store=store, msg=msg, model=_DummyModel())
renamed = store.get_session(sid)
assert renamed is not None
assert str(getattr(renamed, "title", "") or "") == "第三轮标题"
assert str(store.get_setting(f"AIA_SESSION_AUTO_TITLE_STAGE:{sid}") or "") == "3"

View file

@ -3,7 +3,12 @@ from __future__ import annotations
import json
import unittest
from oclaw.runtime.chat.tool_runtime import truncate_tool_result_for_llm_messages, tool_llm_message_max_chars
from oclaw.runtime.chat.tool_runtime import (
compact_turn_tool_messages_for_storage,
tool_llm_message_max_chars,
truncate_tool_result_for_llm_messages,
)
from oclaw.platform.persistence.sqlite_store import SqliteStore
class ToolLlmTruncationTests(unittest.TestCase):
@ -28,6 +33,46 @@ class ToolLlmTruncationTests(unittest.TestCase):
with mock.patch.dict(os.environ, {"AIA_TOOL_LLM_MESSAGE_MAX_CHARS": "9000"}, clear=False):
self.assertEqual(tool_llm_message_max_chars(), 9000)
def test_compact_turn_tool_messages_for_storage(self) -> None:
import tempfile
import uuid
db = f"{tempfile.gettempdir()}/oclaw-test-{uuid.uuid4().hex}.sqlite"
store = SqliteStore(db)
sess = store.create_session("t")
turn_uuid = "turn-1"
store.add_message(
session_id=sess.id,
role="assistant",
content="",
tool_calls=[{"id": "c1", "name": "echo", "arguments": {"x": 1}}],
turn_uuid=turn_uuid,
)
huge = "x" * 200_000
row = store.add_message(
session_id=sess.id,
role="tool",
content=json.dumps({"ok": True, "blob": huge}, ensure_ascii=False),
tool_calls={"tool_call_id": "c1", "name": "echo", "assistant_message_id": 1},
turn_uuid=turn_uuid,
)
before = store.get_messages(session_id=sess.id, limit=20)
before_tool = [m for m in before if m.id == row.id][0]
self.assertNotIn("_truncated_for_llm", str(before_tool.content or ""))
from unittest import mock
with mock.patch.dict("os.environ", {"AIA_TOOL_LLM_MESSAGE_MAX_CHARS": "8000"}, clear=False):
stats = compact_turn_tool_messages_for_storage(
store=store,
session_id=sess.id,
turn_uuid=turn_uuid,
)
self.assertGreaterEqual(int(stats.get("scanned") or 0), 1)
self.assertGreaterEqual(int(stats.get("updated") or 0), 1)
after = store.get_messages(session_id=sess.id, limit=20)
after_tool = [m for m in after if m.id == row.id][0]
self.assertIn("_truncated_for_llm", str(after_tool.content or ""))
if __name__ == "__main__":
unittest.main()

View file

@ -165,3 +165,52 @@ def test_signature_metadata_can_be_forced_on_via_env(monkeypatch) -> None:
tc = (assistant.get("tool_calls") or [])[0]
assert tc.get("extra_content", {}).get("google", {}).get("thought_signature") == "sig_abc"
def test_current_turn_tool_content_not_clipped_with_details_phrase() -> None:
model = RuleBasedChatModel()
rows = [
_Msg(
"assistant",
"",
tool_calls=json.dumps([{"id": "call_now", "name": "t", "arguments": {}}], ensure_ascii=False),
event_type="tool_call",
turn_uuid="turn-now",
),
_Msg(
"tool",
json.dumps({"ok": True, "blob": "x" * 1200}, ensure_ascii=False),
tool_calls=json.dumps({"tool_call_id": "call_now", "name": "t"}, ensure_ascii=False),
event_type="tool_result",
turn_uuid="turn-now",
),
]
msgs = build_llm_messages(store_messages=rows, system_prompt="s", model=model, lang="zh", tool_context_truncate_enabled=True)
tool_rows = [m for m in msgs if m.get("role") == "tool"]
assert tool_rows
assert "详情请重新阅读" not in str(tool_rows[-1].get("content") or "")
def test_historical_tool_content_can_include_details_phrase_when_clamped(monkeypatch) -> None:
monkeypatch.setenv("AIA_REPLAY_TOOL_FULL_ROUNDS", "0")
model = RuleBasedChatModel()
rows = [
_Msg(
"assistant",
"",
tool_calls=json.dumps([{"id": "call_hist", "name": "t", "arguments": {}}], ensure_ascii=False),
event_type="tool_call",
turn_uuid="turn-hist",
),
_Msg(
"tool",
json.dumps({"ok": True, "blob": "x" * 1600}, ensure_ascii=False),
tool_calls=json.dumps({"tool_call_id": "call_hist", "name": "t"}, ensure_ascii=False),
event_type="tool_result",
turn_uuid="turn-hist",
),
]
msgs = build_llm_messages(store_messages=rows, system_prompt="s", model=model, lang="zh", tool_context_truncate_enabled=True)
tool_rows = [m for m in msgs if m.get("role") == "tool"]
assert tool_rows
assert "详情请重新阅读" in str(tool_rows[-1].get("content") or "")

View file

@ -13,12 +13,15 @@ from oclaw.interfaces.http.fastapi_app import create_app
from oclaw.platform.config.paths import db_path
from oclaw.platform.persistence.sqlite_store import SqliteStore
from oclaw.runtime.tools.experts.workspace.fs_tools import list_files_tool, write_file_tool
from oclaw.runtime.tools.experts.workspace.shell_tools import run_command_tool
from oclaw.runtime.tools.experts.workspace.workspace_base import (
access_from_env,
build_workspace_path_access,
clear_workspace_path_access_for_tests,
current_workspace_write_namespace,
resolve_workspace_path,
workspace_path_access_scope,
workspace_write_namespace_scope,
)
@ -111,10 +114,223 @@ class WorkspacePathGuardTests(unittest.TestCase):
with workspace_path_access_scope(None, None):
r = spec.handler({"path": "generated.py", "content": "print('ok')\n", "mode": "overwrite"})
self.assertTrue(r.get("ok"), r)
expected = (self.root / "data" / self.root.name / "generated.py").resolve()
expected = (self.root / "data" / "workspace" / "generated.py").resolve()
self.assertEqual(str(expected), str(r.get("path")))
self.assertTrue(expected.exists())
def test_write_file_relative_path_uses_workspace_namespace_scope(self) -> None:
with mock.patch.dict(
os.environ,
{"OPS_WORKSPACE_ROOT": str(self.root), "OPS_WORKSPACE_EXTRA_ROOTS": "", "OPS_WORKSPACE_ALLOW_ANY_PATH": ""},
clear=False,
):
clear_workspace_path_access_for_tests()
spec = write_file_tool()
with workspace_path_access_scope(None, None), workspace_write_namespace_scope("ops"):
self.assertEqual(current_workspace_write_namespace(), "ops")
r = spec.handler({"path": "generated.py", "content": "print('ok')\n", "mode": "overwrite"})
self.assertTrue(r.get("ok"), r)
expected = (self.root / "data" / "workspace" / "generated.py").resolve()
self.assertEqual(str(expected), str(r.get("path")))
self.assertTrue(expected.exists())
def test_write_file_absolute_path_is_forced_into_workspace_sandbox(self) -> None:
with mock.patch.dict(
os.environ,
{"OPS_WORKSPACE_ROOT": str(self.root), "OPS_WORKSPACE_EXTRA_ROOTS": "", "OPS_WORKSPACE_ALLOW_ANY_PATH": ""},
clear=False,
):
clear_workspace_path_access_for_tests()
spec = write_file_tool()
abs_target = str((self.root / "count_items.py").resolve())
with workspace_path_access_scope(None, None), workspace_write_namespace_scope("ops"):
r = spec.handler({"path": abs_target, "content": "print('ok')\n", "mode": "overwrite"})
self.assertTrue(r.get("ok"), r)
expected = (self.root / "data" / "workspace" / "count_items.py").resolve()
self.assertEqual(str(expected), str(r.get("path")))
self.assertTrue(expected.exists())
def test_run_command_default_cwd_uses_workspace_namespace_sandbox(self) -> None:
with mock.patch.dict(
os.environ,
{
"OPS_WORKSPACE_ROOT": str(self.root),
"OPS_WORKSPACE_EXTRA_ROOTS": "",
"OPS_WORKSPACE_ALLOW_ANY_PATH": "",
"AIA_ENABLE_RUN_COMMAND": "1",
},
clear=False,
):
clear_workspace_path_access_for_tests()
spec = run_command_tool()
with workspace_path_access_scope(None, None), workspace_write_namespace_scope("ops"):
r = spec.handler({"command": "python -c \"print('ok')\""})
self.assertTrue(r.get("ok"), r)
expected_cwd = (self.root / "data" / "workspace").resolve()
self.assertEqual(str(expected_cwd), str(r.get("cwd")))
def test_run_command_strips_leading_cd_chain_in_default_sandbox(self) -> None:
with mock.patch.dict(
os.environ,
{
"OPS_WORKSPACE_ROOT": str(self.root),
"OPS_WORKSPACE_EXTRA_ROOTS": "",
"OPS_WORKSPACE_ALLOW_ANY_PATH": "",
"AIA_ENABLE_RUN_COMMAND": "1",
},
clear=False,
):
clear_workspace_path_access_for_tests()
spec = run_command_tool()
cmd = f'cd /d "{self.root}" && python -c "print(123)"'
with workspace_path_access_scope(None, None), workspace_write_namespace_scope("ops"):
r = spec.handler({"command": cmd})
self.assertTrue(r.get("ok"), r)
self.assertTrue(bool(r.get("normalized_cd_removed")))
expected_cwd = (self.root / "data" / "workspace").resolve()
self.assertEqual(str(expected_cwd), str(r.get("cwd")))
self.assertIn("123", str(r.get("output") or ""))
def test_run_command_strips_windows_drive_prefix_cd_chain(self) -> None:
with mock.patch.dict(
os.environ,
{
"OPS_WORKSPACE_ROOT": str(self.root),
"OPS_WORKSPACE_EXTRA_ROOTS": "",
"OPS_WORKSPACE_ALLOW_ANY_PATH": "",
"AIA_ENABLE_RUN_COMMAND": "1",
},
clear=False,
):
clear_workspace_path_access_for_tests()
ws_root = self.root / "data" / "workspace"
ws_root.mkdir(parents=True, exist_ok=True)
(ws_root / "count_directory.py").write_text("print('drive-cd-ok')\n", encoding="utf-8")
spec = run_command_tool()
cmd = f'D: && cd /d "{self.root}" && python count_directory.py'
with workspace_path_access_scope(None, None):
r = spec.handler({"command": cmd})
self.assertTrue(r.get("ok"), r)
self.assertTrue(bool(r.get("normalized_cd_removed")), r)
self.assertTrue(bool(r.get("script_path_rewritten")), r)
self.assertIn("drive-cd-ok", str(r.get("output") or ""))
def test_run_command_output_flags_distinguish_empty_from_truncation(self) -> None:
with mock.patch.dict(
os.environ,
{
"OPS_WORKSPACE_ROOT": str(self.root),
"OPS_WORKSPACE_EXTRA_ROOTS": "",
"OPS_WORKSPACE_ALLOW_ANY_PATH": "",
"AIA_ENABLE_RUN_COMMAND": "1",
},
clear=False,
):
clear_workspace_path_access_for_tests()
spec = run_command_tool()
with workspace_path_access_scope(None, None), workspace_write_namespace_scope("ops"):
r = spec.handler({"command": 'python -c "pass"'})
self.assertTrue(r.get("ok"), r)
self.assertTrue(bool(r.get("output_empty")))
self.assertFalse(bool(r.get("output_truncated")))
self.assertTrue(bool(r.get("output_not_truncated")))
self.assertEqual(str(r.get("error_code") or ""), "")
def test_run_command_nonzero_exit_marks_failure_not_truncation(self) -> None:
with mock.patch.dict(
os.environ,
{
"OPS_WORKSPACE_ROOT": str(self.root),
"OPS_WORKSPACE_EXTRA_ROOTS": "",
"OPS_WORKSPACE_ALLOW_ANY_PATH": "",
"AIA_ENABLE_RUN_COMMAND": "1",
},
clear=False,
):
clear_workspace_path_access_for_tests()
spec = run_command_tool()
with workspace_path_access_scope(None, None), workspace_write_namespace_scope("ops"):
r = spec.handler({"command": 'python -c "import sys; sys.exit(3)"'})
self.assertFalse(bool(r.get("ok")))
self.assertEqual(int(r.get("exit_code") or 0), 3)
self.assertEqual(str(r.get("error_code") or ""), "command_exit_nonzero")
self.assertFalse(bool(r.get("output_truncated")))
def test_run_command_rewrites_workspace_absolute_script_path_to_sandbox(self) -> None:
with mock.patch.dict(
os.environ,
{
"OPS_WORKSPACE_ROOT": str(self.root),
"OPS_WORKSPACE_EXTRA_ROOTS": "",
"OPS_WORKSPACE_ALLOW_ANY_PATH": "",
"AIA_ENABLE_RUN_COMMAND": "1",
},
clear=False,
):
clear_workspace_path_access_for_tests()
# Prepare script inside sandbox, but command will reference repo-root absolute path.
ws_script = self.root / "data" / "workspace" / "count_files.py"
ws_script.parent.mkdir(parents=True, exist_ok=True)
ws_script.write_text("print('sandbox-ok')\n", encoding="utf-8")
spec = run_command_tool()
absolute_repo_script = str((self.root / "count_files.py").resolve())
with workspace_path_access_scope(None, None), workspace_write_namespace_scope("ops"):
r = spec.handler({"command": f'python "{absolute_repo_script}"'})
self.assertTrue(r.get("ok"), r)
self.assertTrue(bool(r.get("command_rewritten")), r)
self.assertIn("sandbox-ok", str(r.get("output") or ""))
def test_run_command_explicit_repo_root_cwd_is_redirected_to_sandbox(self) -> None:
with mock.patch.dict(
os.environ,
{
"OPS_WORKSPACE_ROOT": str(self.root),
"OPS_WORKSPACE_EXTRA_ROOTS": "",
"OPS_WORKSPACE_ALLOW_ANY_PATH": "",
"AIA_ENABLE_RUN_COMMAND": "1",
},
clear=False,
):
clear_workspace_path_access_for_tests()
ws_script = self.root / "data" / "workspace" / "count_files.py"
ws_script.parent.mkdir(parents=True, exist_ok=True)
ws_script.write_text("print('redirect-ok')\n", encoding="utf-8")
spec = run_command_tool()
abs_repo_script = str((self.root / "count_files.py").resolve())
with workspace_path_access_scope(None, None):
r = spec.handler(
{
"command": f'python "{abs_repo_script}"',
"cwd": str(self.root),
}
)
self.assertTrue(r.get("ok"), r)
self.assertTrue(bool(r.get("cwd_redirected_to_sandbox")), r)
self.assertTrue(bool(r.get("command_rewritten")), r)
self.assertIn("redirect-ok", str(r.get("output") or ""))
def test_run_command_rewrites_relative_python_script_to_sandbox_root(self) -> None:
with mock.patch.dict(
os.environ,
{
"OPS_WORKSPACE_ROOT": str(self.root),
"OPS_WORKSPACE_EXTRA_ROOTS": "",
"OPS_WORKSPACE_ALLOW_ANY_PATH": "",
"AIA_ENABLE_RUN_COMMAND": "1",
},
clear=False,
):
clear_workspace_path_access_for_tests()
ws_root = self.root / "data" / "workspace"
ws_root.mkdir(parents=True, exist_ok=True)
(ws_root / "count_directory.py").write_text("print('found-in-sandbox-root')\n", encoding="utf-8")
spec = run_command_tool()
with workspace_path_access_scope(None, None):
r = spec.handler({"command": "python count_directory.py"})
self.assertTrue(r.get("ok"), r)
self.assertTrue(bool(r.get("script_path_rewritten")), r)
self.assertIn("found-in-sandbox-root", str(r.get("output") or ""))
def test_per_user_extra_roots_from_db(self) -> None:
f = self.extra / "u.txt"
f.write_text("u", encoding="utf-8")