Files
obsidian-wiki/tests/test_mcp_jobs.py
T
Codex 7eb9b987ab feat(mcp): harden remote assets and job execution
Gate source-file downloads and verify registry paths and content hashes. Stream multipart uploads with bounded limits, supervise OpenCode jobs with explicit result markers and timeouts, and preserve managed endpoint security settings. Document the trusted-client boundary and add focused regression coverage.

Validation: PYTHONPATH=<staged-tree> python -m unittest discover -s tests (182 tests)
2026-07-09 21:44:48 +00:00

208 lines
7.0 KiB
Python

import json
import subprocess
import tempfile
import threading
import time
import unittest
from pathlib import Path
from obsidian_wiki.mcp_jobs import (
ProcessSupervisor,
RuntimeLeaseError,
extract_final_answer,
parse_marked_result,
)
class ProcessSupervisorTests(unittest.TestCase):
def test_long_final_answer_keeps_protocol_marker_when_truncated(self):
with tempfile.TemporaryDirectory() as tmp:
log_path = Path(tmp) / "opencode.log"
answer = "MCP_QUERY_OK\n" + ("x" * 20_000)
log_path.write_text(
'{"type":"text","part":{"text":' + json.dumps(answer) + "}}\n",
encoding="utf-8",
)
extracted = extract_final_answer(log_path, limit=12_000)
ok, public_text, error = parse_marked_result(
extracted,
success_marker="MCP_QUERY_OK",
failure_marker="MCP_QUERY_FAILED",
)
self.assertTrue(ok, error)
self.assertTrue(extracted.startswith("MCP_QUERY_OK\n"))
self.assertLessEqual(len(extracted), 12_000)
self.assertEqual(len(public_text), 12_000 - len("MCP_QUERY_OK\n"))
def test_completion_callback_runs_without_client_polling(self):
process = FakeProcess(returncode=0)
completed = threading.Event()
outcomes = []
with tempfile.TemporaryDirectory() as tmp:
supervisor = ProcessSupervisor(
Path(tmp),
popen_factory=lambda *_args, **_kwargs: process,
poll_interval_seconds=0.01,
)
supervisor.start(
"job-1",
["opencode", "run"],
cwd=Path(tmp),
log_path=Path(tmp) / "job.log",
timeout_seconds=30,
on_complete=lambda outcome: (outcomes.append(outcome), completed.set()),
)
self.assertTrue(completed.wait(1))
self.assertEqual(outcomes[0].returncode, 0)
self.assertFalse(outcomes[0].cancelled)
self.assertFalse(outcomes[0].timed_out)
supervisor.close()
def test_completion_callback_failure_invokes_error_fallback(self):
process = FakeProcess(returncode=0)
recovered = threading.Event()
failures = []
with tempfile.TemporaryDirectory() as tmp:
supervisor = ProcessSupervisor(
Path(tmp),
popen_factory=lambda *_args, **_kwargs: process,
poll_interval_seconds=0.01,
)
def fail_callback(_outcome):
raise RuntimeError("callback failed")
supervisor.start(
"job-1",
["opencode", "run"],
cwd=Path(tmp),
log_path=Path(tmp) / "job.log",
timeout_seconds=30,
on_complete=fail_callback,
on_callback_error=lambda outcome, error: (
failures.append((outcome, error)),
recovered.set(),
),
)
self.assertTrue(recovered.wait(1))
self.assertEqual(failures[0][0].job_id, "job-1")
self.assertEqual(str(failures[0][1]), "callback failed")
supervisor.close()
def test_cancel_waits_for_exit_before_reporting_completion(self):
process = FakeProcess(returncode=None, terminate_returncode=-15)
outcomes = []
with tempfile.TemporaryDirectory() as tmp:
supervisor = ProcessSupervisor(
Path(tmp),
popen_factory=lambda *_args, **_kwargs: process,
poll_interval_seconds=0.01,
)
supervisor.start(
"job-1",
["opencode", "run"],
cwd=Path(tmp),
log_path=Path(tmp) / "job.log",
timeout_seconds=30,
on_complete=outcomes.append,
)
self.assertTrue(supervisor.cancel("job-1", grace_seconds=0.1))
self.assertTrue(process.terminated)
self.assertEqual(process.wait_calls, 1)
self.assertEqual(len(outcomes), 1)
self.assertTrue(outcomes[0].cancelled)
self.assertEqual(outcomes[0].returncode, -15)
supervisor.close()
def test_timeout_terminates_process_and_marks_outcome(self):
process = FakeProcess(returncode=None, terminate_returncode=-15)
completed = threading.Event()
outcomes = []
with tempfile.TemporaryDirectory() as tmp:
supervisor = ProcessSupervisor(
Path(tmp),
popen_factory=lambda *_args, **_kwargs: process,
poll_interval_seconds=0.01,
)
supervisor.start(
"job-1",
["opencode", "run"],
cwd=Path(tmp),
log_path=Path(tmp) / "job.log",
timeout_seconds=0.01,
on_complete=lambda outcome: (outcomes.append(outcome), completed.set()),
)
self.assertTrue(completed.wait(1))
self.assertTrue(process.terminated)
self.assertTrue(outcomes[0].timed_out)
supervisor.close()
def test_runtime_root_allows_only_one_supervisor(self):
with tempfile.TemporaryDirectory() as tmp:
first = ProcessSupervisor(Path(tmp), poll_interval_seconds=0.01)
with self.assertRaises(RuntimeLeaseError):
ProcessSupervisor(Path(tmp), poll_interval_seconds=0.01)
first.close()
def test_closed_supervisor_rejects_start_without_spawning(self):
calls = []
with tempfile.TemporaryDirectory() as tmp:
supervisor = ProcessSupervisor(
Path(tmp),
popen_factory=lambda *_args, **_kwargs: calls.append(True),
poll_interval_seconds=0.01,
)
supervisor.close()
with self.assertRaises(RuntimeError):
supervisor.start(
"job-1",
["opencode", "run"],
cwd=Path(tmp),
log_path=Path(tmp) / "job.log",
timeout_seconds=30,
on_complete=lambda _outcome: None,
)
self.assertEqual(calls, [])
class FakeProcess:
def __init__(self, *, returncode, terminate_returncode=-15):
self.returncode = returncode
self.terminate_returncode = terminate_returncode
self.pid = 4242
self.terminated = False
self.killed = False
self.wait_calls = 0
def poll(self):
return self.returncode
def wait(self, timeout=None):
self.wait_calls += 1
if self.returncode is None:
raise subprocess.TimeoutExpired("fake", timeout)
return self.returncode
def terminate(self):
self.terminated = True
self.returncode = self.terminate_returncode
def kill(self):
self.killed = True
self.returncode = -9
if __name__ == "__main__":
unittest.main()