Default ticket, multipart, and text-note ingest entry points to staging without launching a job. Propagate WIKI_ARCHIVE_SOURCES from project configuration into the managed service environment.\n\nValidated with the full 206-test suite.
1042 lines
45 KiB
Python
1042 lines
45 KiB
Python
import base64
|
|
import io
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from obsidian_wiki.mcp_ingest import (
|
|
IngestError,
|
|
IngestManager,
|
|
parse_bool,
|
|
)
|
|
|
|
|
|
class McpIngestTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tempdir = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.tempdir.name)
|
|
self.vault = self.root / "vault"
|
|
self.project = self.root / "project"
|
|
self.runtime = self.project / ".runtime" / "mcp-ingest"
|
|
self.vault.mkdir()
|
|
self.project.mkdir()
|
|
self._write_opencode_skill_fixture(self.project)
|
|
self.manager = IngestManager(
|
|
vault_path=self.vault,
|
|
project_root=self.project,
|
|
runtime_root=self.runtime,
|
|
opencode_path=Path("/usr/bin/opencode"),
|
|
max_file_bytes=100,
|
|
max_chunk_bytes=8,
|
|
)
|
|
|
|
def tearDown(self):
|
|
self.manager.close()
|
|
self.tempdir.cleanup()
|
|
|
|
def test_parse_bool_accepts_enabled_values_only(self):
|
|
self.assertTrue(parse_bool("1"))
|
|
self.assertTrue(parse_bool("true"))
|
|
self.assertTrue(parse_bool("YES"))
|
|
self.assertFalse(parse_bool(""))
|
|
self.assertFalse(parse_bool("0"))
|
|
self.assertFalse(parse_bool("false"))
|
|
|
|
def test_close_still_closes_supervisor_when_queued_job_write_fails(self):
|
|
queued = {"state": "queued"}
|
|
with (
|
|
patch.object(self.manager, "_iter_jobs", return_value=[queued]),
|
|
patch.object(self.manager, "_write_job", side_effect=OSError("disk full")),
|
|
patch.object(self.manager._supervisor, "close") as supervisor_close,
|
|
):
|
|
with self.assertRaises(OSError):
|
|
self.manager.close()
|
|
|
|
supervisor_close.assert_called_once_with()
|
|
|
|
def test_uploads_are_staged_in_runtime_not_vault(self):
|
|
upload = self.manager.create_upload(file_name="drawing.pdf", size_bytes=0, sha256=self._sha256(b""))
|
|
upload_path = self._upload_payload_path(upload["upload_id"]).resolve()
|
|
|
|
self.assertNotIn("path", upload)
|
|
self.assertTrue(upload_path.is_relative_to((self.runtime / "uploads").resolve()))
|
|
self.assertFalse(upload_path.is_relative_to(self.vault.resolve()))
|
|
|
|
def test_runtime_root_must_not_be_inside_vault(self):
|
|
with self.assertRaises(IngestError):
|
|
IngestManager(
|
|
vault_path=self.vault,
|
|
project_root=self.project,
|
|
runtime_root=self.vault / ".runtime" / "mcp-ingest",
|
|
)
|
|
|
|
def test_check_opencode_readiness_static_success(self):
|
|
with patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run) as run_mock:
|
|
result = self.manager.check_opencode_readiness()
|
|
|
|
self.assertTrue(result["ok"])
|
|
self.assertTrue(result["static_preflight"])
|
|
self.assertEqual(result["runtime_smoke_test"], "skipped")
|
|
self.assertEqual(result["opencode_version"], "1.17.13")
|
|
self.assertFalse(result["errors"])
|
|
self.assertTrue(any(path.endswith(".agents/skills/wiki-ingest/SKILL.md") for path in result["checked_paths"]))
|
|
self.assertEqual(run_mock.call_count, 2)
|
|
|
|
def test_check_opencode_readiness_reports_missing_skill_files(self):
|
|
missing_project = self.root / "missing-project"
|
|
missing_project.mkdir()
|
|
(missing_project / "AGENTS.md").write_text("# Agent rules\n", encoding="utf-8")
|
|
manager = IngestManager(
|
|
vault_path=self.vault,
|
|
project_root=missing_project,
|
|
runtime_root=self.root / "runtime-missing",
|
|
opencode_path=Path("/usr/bin/opencode"),
|
|
)
|
|
|
|
with patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run):
|
|
result = manager.check_opencode_readiness()
|
|
|
|
self.assertFalse(result["ok"])
|
|
self.assertFalse(result["static_preflight"])
|
|
self.assertIn(".agents/skills/wiki-ingest/SKILL.md", "\n".join(result["errors"]))
|
|
self.assertIn(".agents/skills/llm-wiki/SKILL.md", "\n".join(result["errors"]))
|
|
|
|
def test_check_opencode_readiness_smoke_test_success(self):
|
|
self.manager.opencode_model = "openai/gpt-5.5"
|
|
self.manager.opencode_variant = "high"
|
|
with patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_smoke_success_run) as run_mock:
|
|
result = self.manager.check_opencode_readiness(run_smoke_test=True)
|
|
|
|
self.assertTrue(result["ok"])
|
|
self.assertEqual(result["runtime_smoke_test"], "passed")
|
|
self.assertEqual(run_mock.call_count, 3)
|
|
smoke_args = run_mock.call_args_list[-1].args[0]
|
|
self.assertEqual(smoke_args[:4], ["/usr/bin/opencode", "run", "--format", "json"])
|
|
self.assertEqual(smoke_args[smoke_args.index("--model") + 1], "openai/gpt-5.5")
|
|
self.assertEqual(smoke_args[smoke_args.index("--variant") + 1], "high")
|
|
self.assertIn("--auto", smoke_args)
|
|
self.assertIn("--dir", smoke_args)
|
|
self.assertIn("MCP ingest readiness smoke test", smoke_args)
|
|
|
|
def test_check_opencode_readiness_smoke_test_failure_and_timeout(self):
|
|
with patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_smoke_failure_run):
|
|
failed = self.manager.check_opencode_readiness(run_smoke_test=True)
|
|
self.assertFalse(failed["ok"])
|
|
self.assertEqual(failed["runtime_smoke_test"], "failed")
|
|
self.assertIn("SKILL_LOAD_FAILED", "\n".join(failed["errors"]))
|
|
|
|
with patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_smoke_timeout_run):
|
|
timed_out = self.manager.check_opencode_readiness(run_smoke_test=True)
|
|
self.assertFalse(timed_out["ok"])
|
|
self.assertEqual(timed_out["runtime_smoke_test"], "failed")
|
|
self.assertIn("timed out", "\n".join(timed_out["errors"]))
|
|
|
|
def test_start_ingest_job_fails_before_opencode_when_static_readiness_fails(self):
|
|
payload = b"source data"
|
|
upload = self._complete_upload("source.pdf", payload)
|
|
(self.project / ".agents" / "skills" / "wiki-ingest" / "SKILL.md").unlink()
|
|
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen") as popen_mock,
|
|
):
|
|
job = self.manager.start_ingest_job([upload["upload_id"]])
|
|
|
|
self.assertEqual(job["state"], "failed")
|
|
self.assertIn("OpenCode readiness failed", job["error"])
|
|
popen_mock.assert_not_called()
|
|
|
|
def test_upload_chunk_lifecycle_verifies_offset_size_and_sha(self):
|
|
payload = b"hello ingest"
|
|
upload = self.manager.create_upload(
|
|
file_name="drawing.pdf",
|
|
size_bytes=len(payload),
|
|
sha256="sha256:" + self._sha256(payload),
|
|
project="bjd",
|
|
doc_type_hint="drawing",
|
|
)
|
|
|
|
first = self.manager.append_upload_chunk(
|
|
upload["upload_id"],
|
|
offset=0,
|
|
data_base64=base64.b64encode(payload[:5]).decode("ascii"),
|
|
)
|
|
self.assertEqual(first["received_bytes"], 5)
|
|
|
|
with self.assertRaises(IngestError):
|
|
self.manager.append_upload_chunk(
|
|
upload["upload_id"],
|
|
offset=3,
|
|
data_base64=base64.b64encode(payload[5:]).decode("ascii"),
|
|
)
|
|
|
|
second = self.manager.append_upload_chunk(
|
|
upload["upload_id"],
|
|
offset=5,
|
|
data_base64=base64.b64encode(payload[5:]).decode("ascii"),
|
|
)
|
|
self.assertEqual(second["received_bytes"], len(payload))
|
|
|
|
complete = self.manager.complete_upload(upload["upload_id"])
|
|
|
|
self.assertEqual(complete["state"], "complete")
|
|
self.assertNotIn("path", complete)
|
|
self.assertEqual(self._upload_payload_path(complete["upload_id"]).read_bytes(), payload)
|
|
|
|
def test_create_completed_upload_accepts_bytes_without_agent_chunking(self):
|
|
payload = b"direct upload payload"
|
|
|
|
upload = self.manager.create_completed_upload(
|
|
file_name="drawing.pdf",
|
|
payload=payload,
|
|
project="bjd",
|
|
doc_type_hint="drawing",
|
|
)
|
|
|
|
self.assertEqual(upload["state"], "complete")
|
|
self.assertEqual(upload["received_bytes"], len(payload))
|
|
self.assertEqual(upload["size_bytes"], len(payload))
|
|
self.assertEqual(upload["sha256"], "sha256:" + self._sha256(payload))
|
|
self.assertEqual(upload["project"], "bjd")
|
|
self.assertEqual(upload["doc_type_hint"], "drawing")
|
|
self.assertNotIn("path", upload)
|
|
self.assertEqual(self._upload_payload_path(upload["upload_id"]).read_bytes(), payload)
|
|
|
|
def test_create_completed_upload_from_stream_reads_bounded_chunks(self):
|
|
payload = b"streamed upload payload" * 100_000
|
|
|
|
class BoundedReadStream(io.BytesIO):
|
|
def __init__(self, data):
|
|
super().__init__(data)
|
|
self.read_sizes = []
|
|
|
|
def read(self, size=-1):
|
|
if size < 0:
|
|
raise AssertionError("stream upload must not use an unbounded read")
|
|
self.read_sizes.append(size)
|
|
return super().read(size)
|
|
|
|
manager = IngestManager(
|
|
vault_path=self.vault,
|
|
project_root=self.project,
|
|
runtime_root=self.root / "stream-runtime",
|
|
max_file_bytes=len(payload) + 1,
|
|
)
|
|
stream = BoundedReadStream(payload)
|
|
|
|
upload = manager.create_completed_upload_from_stream(
|
|
file_name="stream.bin",
|
|
stream=stream,
|
|
size_bytes=len(payload),
|
|
)
|
|
|
|
self.assertEqual(upload["state"], "complete")
|
|
self.assertEqual(upload["sha256"], "sha256:" + self._sha256(payload))
|
|
self.assertGreater(len(stream.read_sizes), 2)
|
|
self.assertTrue(all(size == 1024 * 1024 for size in stream.read_sizes))
|
|
|
|
def test_create_completed_upload_from_stream_cleans_partial_file_when_oversized(self):
|
|
manager = IngestManager(
|
|
vault_path=self.vault,
|
|
project_root=self.project,
|
|
runtime_root=self.root / "small-stream-runtime",
|
|
max_file_bytes=5,
|
|
)
|
|
|
|
with self.assertRaises(IngestError):
|
|
manager.create_completed_upload_from_stream(
|
|
file_name="too-large.bin",
|
|
stream=io.BytesIO(b"123456"),
|
|
)
|
|
|
|
self.assertEqual(list(manager.upload_root.rglob("upload.json")), [])
|
|
self.assertEqual([path for path in manager.upload_root.rglob("*") if path.is_file()], [])
|
|
|
|
def test_upload_ticket_is_single_use_and_preserves_defaults(self):
|
|
ticket = self.manager.create_upload_ticket(
|
|
expires_seconds=60,
|
|
max_files=1,
|
|
project="bjd",
|
|
doc_type_hint="drawing",
|
|
note="customer uploaded drawing",
|
|
bundle_id="drawing-package-1",
|
|
source_type="report_package",
|
|
label="Customer drawing package",
|
|
)
|
|
|
|
self.assertIn("upload_token", ticket)
|
|
self.assertEqual(ticket["max_files"], 1)
|
|
self.assertEqual(ticket["project"], "bjd")
|
|
self.assertEqual(ticket["doc_type_hint"], "drawing")
|
|
self.assertEqual(ticket["note"], "customer uploaded drawing")
|
|
self.assertEqual(ticket["bundle_id"], "drawing-package-1")
|
|
self.assertEqual(ticket["source_type"], "report_package")
|
|
self.assertEqual(ticket["label"], "Customer drawing package")
|
|
self.assertFalse(ticket["start_job"])
|
|
self.assertEqual(ticket["used_at"], None)
|
|
|
|
used = self.manager.consume_upload_ticket(ticket["upload_token"])
|
|
|
|
self.assertEqual(used["project"], "bjd")
|
|
self.assertEqual(used["bundle_id"], "drawing-package-1")
|
|
self.assertIsNotNone(used["used_at"])
|
|
with self.assertRaises(IngestError):
|
|
self.manager.consume_upload_ticket(ticket["upload_token"])
|
|
|
|
def test_upload_rejects_unsafe_names_oversized_files_and_large_chunks(self):
|
|
with self.assertRaises(IngestError):
|
|
self.manager.create_upload(file_name="../secret.pdf", size_bytes=10, sha256="a" * 64)
|
|
|
|
with self.assertRaises(IngestError):
|
|
self.manager.create_upload(file_name="large.pdf", size_bytes=101, sha256="a" * 64)
|
|
|
|
upload = self.manager.create_upload(file_name="ok.pdf", size_bytes=20, sha256=self._sha256(b"x" * 20))
|
|
with self.assertRaises(IngestError):
|
|
self.manager.append_upload_chunk(
|
|
upload["upload_id"],
|
|
offset=0,
|
|
data_base64=base64.b64encode(b"x" * 9).decode("ascii"),
|
|
)
|
|
|
|
with self.assertRaises(IngestError):
|
|
self.manager.create_completed_upload(file_name="too-large.pdf", payload=b"x" * 101)
|
|
|
|
def test_validate_upload_plan_reports_limits_and_exact_duplicates(self):
|
|
duplicate_digest = self._sha256(b"already archived")
|
|
registry = self.vault / "_meta" / "source-registry.jsonl"
|
|
registry.parent.mkdir(parents=True)
|
|
registry.write_text(
|
|
json.dumps(
|
|
{
|
|
"record_type": "source_archive",
|
|
"source_id": "sha256:" + duplicate_digest,
|
|
"archive_path": "_sources/archive/existing.pdf",
|
|
"original_name": "existing.pdf",
|
|
}
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = self.manager.validate_upload_plan(
|
|
[
|
|
{"file_name": "existing.pdf", "size_bytes": 12, "sha256": duplicate_digest},
|
|
{"file_name": "too-large.pdf", "size_bytes": 101, "sha256": "b" * 64},
|
|
{"file_name": "../secret.pdf", "size_bytes": 10, "sha256": "c" * 64},
|
|
{"file_name": "bad-sha.pdf", "size_bytes": 10, "sha256": "not-a-sha"},
|
|
],
|
|
project="bjd",
|
|
doc_type_hint="drawing",
|
|
)
|
|
|
|
self.assertFalse(result["ok"])
|
|
self.assertEqual(result["project"], "bjd")
|
|
self.assertEqual(result["doc_type_hint"], "drawing")
|
|
self.assertEqual(result["max_file_bytes"], 100)
|
|
self.assertEqual(result["max_files_per_job"], 10)
|
|
self.assertEqual(result["files"][0]["status"], "warning")
|
|
self.assertTrue(result["files"][0]["duplicates"])
|
|
self.assertEqual(result["files"][1]["status"], "error")
|
|
self.assertIn("exceeds max upload size", result["files"][1]["errors"][0])
|
|
self.assertIn("unsafe file name", result["files"][2]["errors"][0])
|
|
self.assertIn("sha256", result["files"][3]["errors"][0])
|
|
|
|
def test_preview_upload_metadata_reports_duplicate_and_requires_completed_uploads(self):
|
|
payload = b"duplicate payload"
|
|
digest = self._sha256(payload)
|
|
registry = self.vault / "_meta" / "source-registry.jsonl"
|
|
registry.parent.mkdir(parents=True)
|
|
registry.write_text(
|
|
json.dumps(
|
|
{
|
|
"record_type": "source_archive",
|
|
"source_id": "sha256:" + digest,
|
|
"archive_path": "_sources/archive/duplicate.pdf",
|
|
"original_name": "duplicate.pdf",
|
|
}
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
complete = self.manager.create_completed_upload(
|
|
"duplicate.pdf",
|
|
payload,
|
|
project="bjd",
|
|
doc_type_hint="drawing",
|
|
)
|
|
incomplete = self.manager.create_upload("pending.pdf", 7, self._sha256(b"pending"))
|
|
|
|
preview = self.manager.preview_upload_metadata([complete["upload_id"]])
|
|
|
|
self.assertTrue(preview["ok"])
|
|
self.assertEqual(preview["next_action"], "start_ingest_job")
|
|
self.assertEqual(preview["uploads"][0]["file_extension"], ".pdf")
|
|
self.assertTrue(preview["uploads"][0]["duplicates"])
|
|
self.assertEqual(preview["uploads"][0]["duplicate_count"], 1)
|
|
with self.assertRaises(IngestError):
|
|
self.manager.preview_upload_metadata([incomplete["upload_id"]])
|
|
|
|
def test_start_ingest_job_constructs_restricted_opencode_command(self):
|
|
payload = b"source data"
|
|
upload = self._complete_upload("source.pdf", payload)
|
|
self.manager.create_ingest_bundle(
|
|
[upload["upload_id"]],
|
|
source_type="report_package",
|
|
label="Quality report",
|
|
)
|
|
self.manager.opencode_model = "openai/gpt-5.5"
|
|
self.manager.opencode_variant = "high"
|
|
popen = FakePopen()
|
|
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen", return_value=popen) as popen_mock,
|
|
):
|
|
job = self.manager.start_ingest_job([upload["upload_id"]], note="customer asked for drawings")
|
|
|
|
args = popen_mock.call_args.args[0]
|
|
self.assertEqual(args[:5], ["/usr/bin/opencode", "run", "--format", "json", "--auto"])
|
|
self.assertEqual(args[args.index("--model") + 1], "openai/gpt-5.5")
|
|
self.assertEqual(args[args.index("--variant") + 1], "high")
|
|
self.assertIn("--dir", args)
|
|
self.assertEqual(args[args.index("--dir") + 1], str(self.project))
|
|
self.assertIn("--title", args)
|
|
self.assertIn(f"MCP ingest {job['job_id']}", args)
|
|
prompt = args[-1]
|
|
self.assertIn("/wiki-ingest", prompt)
|
|
self.assertIn('"source_type": "report_package"', prompt)
|
|
self.assertIn('"label": "Quality report"', prompt)
|
|
self.assertIn('"bundle_role": "attachment"', prompt)
|
|
self.assertIn(str(self._upload_payload_path(upload["upload_id"])), prompt)
|
|
self.assertIn("temporary runtime staging files", prompt)
|
|
self.assertIn("Do not ingest any file outside this list.", prompt)
|
|
self.assertIn("obsidian-wiki file-index rebuild", prompt)
|
|
self.assertNotIn("../secret", prompt)
|
|
self.assertNotIn("path", job["files"][0])
|
|
|
|
log_path = Path(self.manager._load_job(job["job_id"])["log_path"])
|
|
log_path.write_text(self._json_text_event("MCP_INGEST_OK\nIngested source.pdf"), encoding="utf-8")
|
|
popen.returncode = 0
|
|
with patch(
|
|
"obsidian_wiki.mcp_ingest.verify_ingest_outputs",
|
|
return_value=self._passed_verification(1),
|
|
):
|
|
self.manager.refresh_jobs()
|
|
status = self.manager.get_job_status(job["job_id"])
|
|
self.assertEqual(status["state"], "succeeded")
|
|
self.assertEqual(status["exit_code"], 0)
|
|
self.assertNotIn("path", status["files"][0])
|
|
|
|
def test_job_reports_authoritative_preflight_and_agent_running_phases(self):
|
|
upload = self._complete_upload("source.pdf", b"source data")
|
|
popen = FakePopen(returncode=None)
|
|
observed = {}
|
|
|
|
def readiness_during_launch(*, run_smoke_test=False):
|
|
internal = next(iter(self.manager._iter_jobs()))
|
|
observed["state"] = internal["state"]
|
|
observed["phase"] = internal["phase"]
|
|
observed["verification"] = internal["verification"]
|
|
lock_probe = []
|
|
|
|
def acquire_manager_lock():
|
|
acquired = self.manager._lock.acquire(timeout=0.2)
|
|
lock_probe.append(acquired)
|
|
if acquired:
|
|
self.manager._lock.release()
|
|
|
|
thread = threading.Thread(target=acquire_manager_lock)
|
|
thread.start()
|
|
thread.join()
|
|
observed["status_lock_available"] = lock_probe == [True]
|
|
return {
|
|
"ok": True,
|
|
"errors": [],
|
|
"warnings": [],
|
|
"static_preflight": True,
|
|
"runtime_smoke_test": "skipped",
|
|
}
|
|
|
|
with (
|
|
patch.object(self.manager, "check_opencode_readiness", side_effect=readiness_during_launch),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen", return_value=popen),
|
|
):
|
|
status = self.manager.start_ingest_job([upload["upload_id"]])
|
|
|
|
self.assertEqual(observed["state"], "running")
|
|
self.assertEqual(observed["phase"], "preflight")
|
|
self.assertEqual(observed["verification"]["status"], "not_run")
|
|
self.assertTrue(observed["status_lock_available"])
|
|
self.assertEqual(status["state"], "running")
|
|
self.assertEqual(status["phase"], "agent_running")
|
|
self.assertTrue(status["phase_started_at"])
|
|
self.assertTrue(status["process_alive"])
|
|
self.assertEqual(status["verification"]["status"], "not_run")
|
|
|
|
def test_queued_and_cancelled_job_phases_are_authoritative(self):
|
|
first = self._complete_upload("first.pdf", b"first")
|
|
second = self._complete_upload("second.pdf", b"second")
|
|
popen = FakePopen(returncode=None)
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen", return_value=popen),
|
|
):
|
|
self.manager.start_ingest_job([first["upload_id"]])
|
|
queued = self.manager.start_ingest_job([second["upload_id"]])
|
|
|
|
self.assertEqual(queued["state"], "queued")
|
|
self.assertEqual(queued["phase"], "queued")
|
|
self.assertFalse(queued["process_alive"])
|
|
self.assertEqual(queued["verification"]["status"], "not_run")
|
|
|
|
cancelled = self.manager.cancel_ingest_job(queued["job_id"])
|
|
self.assertEqual(cancelled["state"], "cancelled")
|
|
self.assertEqual(cancelled["phase"], "complete")
|
|
self.assertFalse(cancelled["process_alive"])
|
|
self.assertEqual(cancelled["verification"]["status"], "not_run")
|
|
|
|
def test_preflight_failure_completes_without_verification(self):
|
|
upload = self._complete_upload("source.pdf", b"source")
|
|
(self.project / ".agents" / "skills" / "wiki-ingest" / "SKILL.md").unlink()
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen") as popen_mock,
|
|
):
|
|
status = self.manager.start_ingest_job([upload["upload_id"]])
|
|
|
|
self.assertEqual(status["state"], "failed")
|
|
self.assertEqual(status["phase"], "complete")
|
|
self.assertFalse(status["process_alive"])
|
|
self.assertEqual(status["verification"]["status"], "not_run")
|
|
popen_mock.assert_not_called()
|
|
|
|
def test_preflight_job_can_be_cancelled_while_readiness_runs(self):
|
|
upload = self._complete_upload("source.pdf", b"source")
|
|
cancelled = {}
|
|
|
|
def cancel_during_readiness(*, run_smoke_test=False):
|
|
internal = next(iter(self.manager._iter_jobs()))
|
|
|
|
def cancel_job():
|
|
cancelled.update(self.manager.cancel_ingest_job(internal["job_id"]))
|
|
|
|
thread = threading.Thread(target=cancel_job)
|
|
thread.start()
|
|
thread.join()
|
|
return {
|
|
"ok": True,
|
|
"errors": [],
|
|
"warnings": [],
|
|
"static_preflight": True,
|
|
"runtime_smoke_test": "skipped",
|
|
}
|
|
|
|
with (
|
|
patch.object(self.manager, "check_opencode_readiness", side_effect=cancel_during_readiness),
|
|
patch(
|
|
"obsidian_wiki.mcp_ingest.subprocess.Popen",
|
|
return_value=FakePopen(returncode=None),
|
|
) as popen_mock,
|
|
):
|
|
status = self.manager.start_ingest_job([upload["upload_id"]])
|
|
|
|
self.assertEqual(cancelled["state"], "cancelled")
|
|
self.assertEqual(status["state"], "cancelled")
|
|
self.assertEqual(status["phase"], "complete")
|
|
self.assertEqual(status["verification"]["status"], "not_run")
|
|
popen_mock.assert_not_called()
|
|
|
|
def test_unexpected_preflight_error_finishes_job(self):
|
|
upload = self._complete_upload("source.pdf", b"source")
|
|
with (
|
|
patch.object(
|
|
self.manager,
|
|
"check_opencode_readiness",
|
|
side_effect=RuntimeError("readiness crashed"),
|
|
),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen") as popen_mock,
|
|
):
|
|
status = self.manager.start_ingest_job([upload["upload_id"]])
|
|
|
|
self.assertEqual(status["state"], "failed")
|
|
self.assertEqual(status["phase"], "complete")
|
|
self.assertEqual(status["verification"]["status"], "not_run")
|
|
self.assertIn("readiness crashed", status["error"])
|
|
popen_mock.assert_not_called()
|
|
|
|
def test_legacy_job_records_derive_phase_and_verification_defaults(self):
|
|
upload = self._complete_upload("source.pdf", b"source")
|
|
(self.project / ".agents" / "skills" / "wiki-ingest" / "SKILL.md").unlink()
|
|
with patch(
|
|
"obsidian_wiki.mcp_ingest.subprocess.run",
|
|
side_effect=self._fake_static_readiness_run,
|
|
):
|
|
status = self.manager.start_ingest_job([upload["upload_id"]])
|
|
|
|
internal = self.manager._load_job(status["job_id"])
|
|
internal.pop("phase", None)
|
|
internal.pop("phase_started_at", None)
|
|
internal.pop("verification", None)
|
|
self.manager._write_job(internal)
|
|
|
|
legacy = self.manager.get_ingest_job_status(status["job_id"])
|
|
self.assertEqual(legacy["phase"], "complete")
|
|
self.assertEqual(legacy["verification"]["status"], "not_run")
|
|
self.assertFalse(legacy["process_alive"])
|
|
|
|
def test_service_restart_completes_interrupted_job_without_verification(self):
|
|
upload = self._complete_upload("source.pdf", b"source")
|
|
(self.project / ".agents" / "skills" / "wiki-ingest" / "SKILL.md").unlink()
|
|
with patch(
|
|
"obsidian_wiki.mcp_ingest.subprocess.run",
|
|
side_effect=self._fake_static_readiness_run,
|
|
):
|
|
status = self.manager.start_ingest_job([upload["upload_id"]])
|
|
|
|
internal = self.manager._load_job(status["job_id"])
|
|
internal["state"] = "running"
|
|
internal["phase"] = "verifying"
|
|
internal["verification"] = {"status": "running"}
|
|
internal["finished_at"] = None
|
|
self.manager._write_job(internal)
|
|
self.manager._mark_interrupted_jobs()
|
|
|
|
restarted = self.manager.get_ingest_job_status(status["job_id"])
|
|
self.assertEqual(restarted["state"], "failed")
|
|
self.assertEqual(restarted["phase"], "complete")
|
|
self.assertEqual(restarted["verification"]["status"], "not_run")
|
|
self.assertIn("service restart", restarted["error"])
|
|
|
|
def test_success_marker_runs_verification_before_succeeding(self):
|
|
upload = self._complete_upload("source.pdf", b"source")
|
|
popen = FakePopen(returncode=None)
|
|
observed = {}
|
|
passed = {
|
|
"status": "passed",
|
|
"checked_at": "2026-07-16T10:01:00Z",
|
|
"failures": [],
|
|
"warnings": [],
|
|
"outputs": {
|
|
"source_count": 1,
|
|
"registry_count": 1,
|
|
"archive_count": 1,
|
|
"manifest_count": 1,
|
|
"indexed_count": 1,
|
|
},
|
|
}
|
|
|
|
def verify_while_observable(**kwargs):
|
|
internal = self.manager._load_job(job["job_id"])
|
|
public = self.manager._public_job(internal)
|
|
observed["phase"] = public["phase"]
|
|
observed["process_alive"] = public["process_alive"]
|
|
observed["verification"] = public["verification"]
|
|
observed["uploads"] = kwargs["uploads"]
|
|
return passed
|
|
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen", return_value=popen),
|
|
):
|
|
job = self.manager.start_ingest_job([upload["upload_id"]])
|
|
log_path = Path(self.manager._load_job(job["job_id"])["log_path"])
|
|
log_path.write_text(
|
|
self._json_text_event("MCP_INGEST_OK\nIngested source.pdf"),
|
|
encoding="utf-8",
|
|
)
|
|
popen.returncode = 0
|
|
with patch(
|
|
"obsidian_wiki.mcp_ingest.verify_ingest_outputs",
|
|
side_effect=verify_while_observable,
|
|
):
|
|
self.manager.refresh_jobs()
|
|
|
|
status = self.manager.get_ingest_job_status(job["job_id"])
|
|
self.assertEqual(observed["phase"], "verifying")
|
|
self.assertFalse(observed["process_alive"])
|
|
self.assertEqual(observed["verification"]["status"], "running")
|
|
self.assertEqual(observed["uploads"][0]["path"], str(self._upload_payload_path(upload["upload_id"])))
|
|
self.assertEqual(status["state"], "succeeded")
|
|
self.assertEqual(status["phase"], "complete")
|
|
self.assertFalse(status["process_alive"])
|
|
self.assertEqual(status["verification"], passed)
|
|
|
|
def test_failed_verification_overrides_agent_success_and_preserves_output(self):
|
|
upload = self._complete_upload("source.pdf", b"source")
|
|
popen = FakePopen(returncode=None)
|
|
failed = {
|
|
"status": "failed",
|
|
"checked_at": "2026-07-16T10:01:00Z",
|
|
"failures": [{"code": "manifest_missing", "upload_id": upload["upload_id"]}],
|
|
"warnings": [],
|
|
"outputs": {
|
|
"source_count": 1,
|
|
"registry_count": 1,
|
|
"archive_count": 1,
|
|
"manifest_count": 0,
|
|
"indexed_count": 1,
|
|
},
|
|
}
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen", return_value=popen),
|
|
):
|
|
job = self.manager.start_ingest_job([upload["upload_id"]])
|
|
log_path = Path(self.manager._load_job(job["job_id"])["log_path"])
|
|
log_path.write_text(
|
|
self._json_text_event("MCP_INGEST_OK\nCreated references/source.md"),
|
|
encoding="utf-8",
|
|
)
|
|
popen.returncode = 0
|
|
with patch(
|
|
"obsidian_wiki.mcp_ingest.verify_ingest_outputs",
|
|
return_value=failed,
|
|
):
|
|
self.manager.refresh_jobs()
|
|
|
|
result = self.manager.get_ingest_job_result(job["job_id"])
|
|
self.assertEqual(result["state"], "failed")
|
|
self.assertEqual(result["phase"], "complete")
|
|
self.assertEqual(result["verification"], failed)
|
|
self.assertIn("verification failed", result["error"])
|
|
self.assertIn("Created references/source.md", result["opencode_output_tail"])
|
|
|
|
def test_protocol_failure_skips_verification(self):
|
|
upload = self._complete_upload("source.pdf", b"source")
|
|
popen = FakePopen(returncode=None)
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen", return_value=popen),
|
|
):
|
|
job = self.manager.start_ingest_job([upload["upload_id"]])
|
|
log_path = Path(self.manager._load_job(job["job_id"])["log_path"])
|
|
log_path.write_text(
|
|
self._json_text_event("MCP_INGEST_FAILED\nagent failed"),
|
|
encoding="utf-8",
|
|
)
|
|
popen.returncode = 0
|
|
with patch("obsidian_wiki.mcp_ingest.verify_ingest_outputs") as verify_mock:
|
|
self.manager.refresh_jobs()
|
|
|
|
status = self.manager.get_ingest_job_status(job["job_id"])
|
|
self.assertEqual(status["state"], "failed")
|
|
self.assertEqual(status["phase"], "complete")
|
|
self.assertEqual(status["verification"]["status"], "not_run")
|
|
verify_mock.assert_not_called()
|
|
|
|
def test_ingest_result_rejects_skill_failure_with_zero_exit_code(self):
|
|
upload = self._complete_upload("source.pdf", b"source data")
|
|
popen = FakePopen(returncode=None)
|
|
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen", return_value=popen),
|
|
):
|
|
job = self.manager.start_ingest_job([upload["upload_id"]])
|
|
|
|
log_path = Path(self.manager._load_job(job["job_id"])["log_path"])
|
|
log_path.write_text(self._json_text_event("MCP_INGEST_FAILED\nSKILL_LOAD_FAILED: missing"), encoding="utf-8")
|
|
popen.returncode = 0
|
|
result = self.manager.get_ingest_job_result(job["job_id"])
|
|
|
|
self.assertEqual(result["state"], "failed")
|
|
self.assertIn("SKILL_LOAD_FAILED", result["error"])
|
|
|
|
def test_ingest_public_status_hides_raw_tool_logs_paths_and_pid(self):
|
|
upload = self._complete_upload("source.pdf", b"source data")
|
|
popen = FakePopen(returncode=None)
|
|
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen", return_value=popen),
|
|
):
|
|
job = self.manager.start_ingest_job([upload["upload_id"]])
|
|
|
|
log_path = Path(self.manager._load_job(job["job_id"])["log_path"])
|
|
log_path.write_text(
|
|
'{"type":"tool_use","part":{"state":{"output":"/home/private/secret"}}}\n'
|
|
+ self._json_text_event("MCP_INGEST_OK\nIngest complete"),
|
|
encoding="utf-8",
|
|
)
|
|
popen.returncode = 0
|
|
with patch(
|
|
"obsidian_wiki.mcp_ingest.verify_ingest_outputs",
|
|
return_value=self._passed_verification(1),
|
|
):
|
|
result = self.manager.get_ingest_job_result(job["job_id"])
|
|
status = self.manager.get_ingest_job_status(job["job_id"])
|
|
|
|
self.assertEqual(result["state"], "succeeded")
|
|
self.assertEqual(result["opencode_output_tail"], "Ingest complete")
|
|
self.assertNotIn("/home/private", result["opencode_output_tail"])
|
|
self.assertNotIn("pid", status)
|
|
self.assertNotIn("checked_paths", status["preflight"])
|
|
|
|
internal = self.manager._load_job(job["job_id"])
|
|
internal["error"] = f"failed while reading {self.project / 'secret.txt'}"
|
|
internal["preflight"]["warnings"] = [f"check {self.runtime / 'private.log'}"]
|
|
self.manager._write_job(internal)
|
|
redacted = self.manager.get_ingest_job_status(job["job_id"])
|
|
serialized = json.dumps(redacted)
|
|
self.assertNotIn(str(self.project), serialized)
|
|
self.assertNotIn(str(self.runtime), serialized)
|
|
self.assertIn("<project>", serialized)
|
|
self.assertIn("<runtime>", serialized)
|
|
|
|
def test_jobs_queue_one_at_a_time_and_cancel_queued_job(self):
|
|
first = self._complete_upload("first.pdf", b"first")
|
|
second = self._complete_upload("second.pdf", b"second")
|
|
popen = FakePopen(returncode=None)
|
|
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen", return_value=popen),
|
|
):
|
|
first_job = self.manager.start_ingest_job([first["upload_id"]])
|
|
second_job = self.manager.start_ingest_job([second["upload_id"]])
|
|
|
|
self.assertEqual(self.manager.get_job_status(first_job["job_id"])["state"], "running")
|
|
self.assertEqual(self.manager.get_job_status(second_job["job_id"])["state"], "queued")
|
|
|
|
cancelled = self.manager.cancel_ingest_job(second_job["job_id"])
|
|
|
|
self.assertEqual(cancelled["state"], "cancelled")
|
|
|
|
def test_start_ingest_job_is_idempotent_for_claimed_uploads(self):
|
|
upload = self._complete_upload("source.pdf", b"source")
|
|
popen = FakePopen(returncode=None)
|
|
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen", return_value=popen) as popen_mock,
|
|
):
|
|
first = self.manager.start_ingest_job([upload["upload_id"]])
|
|
repeated = self.manager.start_ingest_job([upload["upload_id"]])
|
|
|
|
self.assertEqual(repeated["job_id"], first["job_id"])
|
|
self.assertEqual(repeated["state"], "running")
|
|
popen_mock.assert_called_once()
|
|
|
|
def test_create_bundle_persists_body_and_attachment_relationship(self):
|
|
body = self._complete_upload("thread.md", b"# Thread\n\nBody")
|
|
attachment = self._complete_upload("report.pdf", b"report")
|
|
|
|
bundle = self.manager.create_ingest_bundle(
|
|
[body["upload_id"], attachment["upload_id"]],
|
|
body_upload_id=body["upload_id"],
|
|
bundle_id="mail-thread-123",
|
|
source_type="email_thread",
|
|
label="Customer quality thread",
|
|
)
|
|
|
|
self.assertEqual(bundle["bundle_id"], "mail-thread-123")
|
|
self.assertEqual(bundle["source_type"], "email_thread")
|
|
self.assertEqual(bundle["label"], "Customer quality thread")
|
|
self.assertEqual(bundle["body_upload_id"], body["upload_id"])
|
|
self.assertEqual(bundle["attachment_upload_ids"], [attachment["upload_id"]])
|
|
self.assertEqual(
|
|
self.manager._load_upload(body["upload_id"])["bundle_role"],
|
|
"primary",
|
|
)
|
|
self.assertEqual(
|
|
self.manager._load_upload(attachment["upload_id"])["bundle_role"],
|
|
"attachment",
|
|
)
|
|
|
|
preview = self.manager.preview_upload_metadata(
|
|
[body["upload_id"], attachment["upload_id"]]
|
|
)
|
|
self.assertEqual(preview["bundle"], bundle)
|
|
|
|
def test_start_ingest_job_rejects_partial_bundle(self):
|
|
body = self._complete_upload("thread.md", b"body")
|
|
attachment = self._complete_upload("report.pdf", b"report")
|
|
self.manager.create_ingest_bundle(
|
|
[body["upload_id"], attachment["upload_id"]],
|
|
body_upload_id=body["upload_id"],
|
|
)
|
|
|
|
with self.assertRaisesRegex(IngestError, "complete bundle"):
|
|
self.manager.start_ingest_job([body["upload_id"]])
|
|
|
|
def test_start_ingest_job_rejects_duplicate_upload_ids(self):
|
|
upload = self._complete_upload("source.pdf", b"source")
|
|
|
|
with self.assertRaisesRegex(IngestError, "upload_ids must be unique"):
|
|
self.manager.start_ingest_job([upload["upload_id"], upload["upload_id"]])
|
|
|
|
def test_cleanup_runtime_prunes_expired_tickets_and_old_completed_artifacts(self):
|
|
upload = self._complete_upload("old.pdf", b"old payload")
|
|
ticket = self.manager.create_upload_ticket(expires_seconds=60, max_files=1)
|
|
ticket_path = next(self.manager.upload_ticket_root.glob("*.json"))
|
|
ticket_meta = json.loads(ticket_path.read_text(encoding="utf-8"))
|
|
ticket_meta["expires_epoch"] = time.time() - 10
|
|
ticket_path.write_text(json.dumps(ticket_meta), encoding="utf-8")
|
|
|
|
popen = FakePopen(returncode=0)
|
|
with (
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.run", side_effect=self._fake_static_readiness_run),
|
|
patch("obsidian_wiki.mcp_ingest.subprocess.Popen", return_value=popen),
|
|
):
|
|
job = self.manager.start_ingest_job([upload["upload_id"]])
|
|
self.manager.refresh_jobs()
|
|
|
|
old_epoch = time.time() - 40 * 24 * 60 * 60
|
|
upload_path = self._upload_payload_path(upload["upload_id"])
|
|
upload_dir = upload_path.parent
|
|
job_dir = self.manager.jobs_root / job["job_id"]
|
|
bundle_dir = self.manager.bundles_root / job["bundle"]["bundle_id"]
|
|
self._touch_tree(upload_dir, old_epoch)
|
|
self._touch_tree(job_dir, old_epoch)
|
|
self._touch_tree(bundle_dir, old_epoch)
|
|
os.utime(ticket_path, (old_epoch, old_epoch))
|
|
|
|
result = self.manager.cleanup_runtime(max_age_days=30)
|
|
|
|
self.assertGreaterEqual(result["expired_tickets_deleted"], 1)
|
|
self.assertEqual(result["old_uploads_deleted"], 1)
|
|
self.assertEqual(result["old_bundles_deleted"], 1)
|
|
self.assertEqual(result["old_jobs_deleted"], 1)
|
|
self.assertFalse(upload_dir.exists())
|
|
self.assertFalse(bundle_dir.exists())
|
|
self.assertFalse(job_dir.exists())
|
|
self.assertFalse(ticket_path.exists())
|
|
|
|
def _complete_upload(self, name: str, payload: bytes) -> dict:
|
|
upload = self.manager.create_upload(name, len(payload), self._sha256(payload))
|
|
offset = 0
|
|
while offset < len(payload):
|
|
chunk = payload[offset : offset + self.manager.max_chunk_bytes]
|
|
self.manager.append_upload_chunk(
|
|
upload["upload_id"],
|
|
offset=offset,
|
|
data_base64=base64.b64encode(chunk).decode("ascii"),
|
|
)
|
|
offset += len(chunk)
|
|
return self.manager.complete_upload(upload["upload_id"])
|
|
|
|
def _upload_payload_path(self, upload_id: str) -> Path:
|
|
return Path(self.manager._load_upload(upload_id)["path"])
|
|
|
|
def _touch_tree(self, path: Path, epoch: float) -> None:
|
|
for child in path.rglob("*"):
|
|
os.utime(child, (epoch, epoch))
|
|
os.utime(path, (epoch, epoch))
|
|
|
|
def _sha256(self, payload: bytes) -> str:
|
|
import hashlib
|
|
|
|
return hashlib.sha256(payload).hexdigest()
|
|
|
|
def _write_opencode_skill_fixture(self, root: Path) -> None:
|
|
(root / "AGENTS.md").write_text("# Agent rules\n", encoding="utf-8")
|
|
wiki_ingest = root / ".agents" / "skills" / "wiki-ingest"
|
|
llm_wiki = root / ".agents" / "skills" / "llm-wiki"
|
|
wiki_ingest_refs = wiki_ingest / "references"
|
|
wiki_ingest_refs.mkdir(parents=True)
|
|
llm_wiki.mkdir(parents=True)
|
|
(wiki_ingest / "SKILL.md").write_text(
|
|
"---\nname: wiki-ingest\ndescription: Ingest documents into the wiki.\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
(llm_wiki / "SKILL.md").write_text(
|
|
"---\nname: llm-wiki\ndescription: Shared wiki framework instructions.\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
(wiki_ingest_refs / "ingest-prompts.md").write_text("# Prompt templates\n", encoding="utf-8")
|
|
|
|
def _fake_static_readiness_run(self, args, **_kwargs):
|
|
if args == ["/usr/bin/opencode", "--version"]:
|
|
return subprocess.CompletedProcess(args, 0, stdout="1.17.13\n", stderr="")
|
|
if args == ["/usr/bin/opencode", "run", "--help"]:
|
|
return subprocess.CompletedProcess(
|
|
args,
|
|
0,
|
|
stdout="--format\n--auto\n--dir\n--title\n",
|
|
stderr="",
|
|
)
|
|
raise AssertionError(f"unexpected subprocess.run args: {args}")
|
|
|
|
def _fake_smoke_success_run(self, args, **kwargs):
|
|
if args in (["/usr/bin/opencode", "--version"], ["/usr/bin/opencode", "run", "--help"]):
|
|
return self._fake_static_readiness_run(args, **kwargs)
|
|
if args[:3] == ["/usr/bin/opencode", "run", "--format"]:
|
|
return subprocess.CompletedProcess(args, 0, stdout='{"message":"WIKI_INGEST_SKILL_LOADED"}\n', stderr="")
|
|
raise AssertionError(f"unexpected subprocess.run args: {args}")
|
|
|
|
def _fake_smoke_failure_run(self, args, **kwargs):
|
|
if args in (["/usr/bin/opencode", "--version"], ["/usr/bin/opencode", "run", "--help"]):
|
|
return self._fake_static_readiness_run(args, **kwargs)
|
|
if args[:3] == ["/usr/bin/opencode", "run", "--format"]:
|
|
return subprocess.CompletedProcess(args, 0, stdout="SKILL_LOAD_FAILED: missing\n", stderr="")
|
|
raise AssertionError(f"unexpected subprocess.run args: {args}")
|
|
|
|
def _fake_smoke_timeout_run(self, args, **kwargs):
|
|
if args in (["/usr/bin/opencode", "--version"], ["/usr/bin/opencode", "run", "--help"]):
|
|
return self._fake_static_readiness_run(args, **kwargs)
|
|
if args[:3] == ["/usr/bin/opencode", "run", "--format"]:
|
|
raise subprocess.TimeoutExpired(args, kwargs.get("timeout"))
|
|
raise AssertionError(f"unexpected subprocess.run args: {args}")
|
|
|
|
def _passed_verification(self, source_count):
|
|
return {
|
|
"status": "passed",
|
|
"checked_at": "2026-07-16T10:01:00Z",
|
|
"failures": [],
|
|
"warnings": [],
|
|
"outputs": {
|
|
"source_count": source_count,
|
|
"registry_count": source_count,
|
|
"archive_count": source_count,
|
|
"manifest_count": source_count,
|
|
"indexed_count": source_count,
|
|
},
|
|
}
|
|
|
|
def _json_text_event(self, text):
|
|
return json.dumps(
|
|
{
|
|
"type": "text",
|
|
"part": {
|
|
"type": "text",
|
|
"text": text,
|
|
"metadata": {"openai": {"phase": "final_answer"}},
|
|
},
|
|
}
|
|
) + "\n"
|
|
|
|
|
|
class FakePopen:
|
|
def __init__(self, returncode=None):
|
|
self.returncode = returncode
|
|
self.pid = 12345
|
|
self.terminated = False
|
|
|
|
def poll(self):
|
|
return self.returncode
|
|
|
|
def wait(self, timeout=None):
|
|
deadline = time.monotonic() + (timeout or 0)
|
|
while self.returncode is None and (timeout is None or time.monotonic() < deadline):
|
|
time.sleep(0.01)
|
|
if self.returncode is None:
|
|
raise subprocess.TimeoutExpired("fake", timeout)
|
|
return self.returncode
|
|
|
|
def terminate(self):
|
|
self.terminated = True
|
|
self.returncode = -15
|
|
|
|
def kill(self):
|
|
self.returncode = -9
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|