feat(mcp): add verified ingest job phases
Expose authoritative ingest phases and supervisor-backed process liveness without semantic activity inference. Require deterministic registry, manifest, archive, file-index, and declared-output verification before an agent-successful job can finish succeeded.\n\nPreserve legacy state values, derive defaults for older job records, keep verification outside the manager lock, and retain partial outputs on verification failure.\n\nValidated with the full unittest suite (205 tests).
This commit is contained in:
+324
-3
@@ -4,6 +4,7 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
@@ -423,12 +424,313 @@ class McpIngestTests(unittest.TestCase):
|
||||
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
|
||||
self.manager.refresh_jobs()
|
||||
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)
|
||||
@@ -464,8 +766,12 @@ class McpIngestTests(unittest.TestCase):
|
||||
encoding="utf-8",
|
||||
)
|
||||
popen.returncode = 0
|
||||
result = self.manager.get_ingest_job_result(job["job_id"])
|
||||
status = self.manager.get_ingest_job_status(job["job_id"])
|
||||
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")
|
||||
@@ -679,6 +985,21 @@ class McpIngestTests(unittest.TestCase):
|
||||
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(
|
||||
{
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import hashlib
|
||||
import json
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from obsidian_wiki.mcp_ingest_verification import verify_ingest_outputs
|
||||
|
||||
|
||||
class McpIngestVerificationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tempdir = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.tempdir.name)
|
||||
self.vault = self.root / "vault"
|
||||
self.runtime = self.root / "runtime"
|
||||
self.vault.mkdir()
|
||||
self.runtime.mkdir()
|
||||
(self.vault / "_meta").mkdir()
|
||||
self.payload = b"verified source"
|
||||
self.digest = hashlib.sha256(self.payload).hexdigest()
|
||||
self.upload_path = self.runtime / "source.pdf"
|
||||
self.upload_path.write_bytes(self.payload)
|
||||
self.upload = {
|
||||
"upload_id": "upload-1",
|
||||
"path": str(self.upload_path),
|
||||
"sha256": f"sha256:{self.digest}",
|
||||
}
|
||||
self.started_at = "2026-07-16T10:00:00Z"
|
||||
self.archive_path = "_sources/archive/project/2026-07/source.pdf"
|
||||
self.registry_record = {
|
||||
"source_id": f"sha256:{self.digest}",
|
||||
"archive_path": self.archive_path,
|
||||
"related_pages": ["references/source.md"],
|
||||
"catalog_paths": ["_sources/catalog/source.md"],
|
||||
}
|
||||
self.manifest_record = {
|
||||
"ingested_at": "2026-07-16T10:01:00Z",
|
||||
"content_hash": f"sha256:{self.digest}",
|
||||
"source_id": f"sha256:{self.digest}",
|
||||
"original_path": str(self.upload_path),
|
||||
"archive_path": self.archive_path,
|
||||
"pages_created": ["references/source.md"],
|
||||
"pages_updated": [],
|
||||
"catalog_paths": ["_sources/catalog/source.md"],
|
||||
}
|
||||
self._write_complete_outputs()
|
||||
|
||||
def tearDown(self):
|
||||
self.tempdir.cleanup()
|
||||
|
||||
def _write_complete_outputs(self):
|
||||
archive = self.vault / self.archive_path
|
||||
archive.parent.mkdir(parents=True, exist_ok=True)
|
||||
archive.write_bytes(self.payload)
|
||||
for relative_path in ("references/source.md", "_sources/catalog/source.md"):
|
||||
path = self.vault / relative_path
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("verified", encoding="utf-8")
|
||||
registry = self.vault / "_meta" / "source-registry.jsonl"
|
||||
registry.write_text(json.dumps(self.registry_record) + "\n", encoding="utf-8")
|
||||
manifest = {"version": 1, "sources": {str(self.upload_path): self.manifest_record}}
|
||||
(self.vault / ".manifest.json").write_text(
|
||||
json.dumps(manifest),
|
||||
encoding="utf-8",
|
||||
)
|
||||
db_path = self.vault / "_meta" / "file-assets.sqlite"
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
conn.execute("CREATE TABLE file_assets (sha256 TEXT NOT NULL)")
|
||||
conn.execute("INSERT INTO file_assets (sha256) VALUES (?)", (self.digest,))
|
||||
|
||||
def _verify(self):
|
||||
return verify_ingest_outputs(
|
||||
vault_path=self.vault,
|
||||
uploads=[self.upload],
|
||||
job_started_at=self.started_at,
|
||||
archive_sources=True,
|
||||
)
|
||||
|
||||
def test_passes_when_all_deterministic_outputs_exist(self):
|
||||
result = self._verify()
|
||||
|
||||
self.assertEqual(result["status"], "passed")
|
||||
self.assertEqual(result["failures"], [])
|
||||
self.assertEqual(
|
||||
result["outputs"],
|
||||
{
|
||||
"source_count": 1,
|
||||
"registry_count": 1,
|
||||
"archive_count": 1,
|
||||
"manifest_count": 1,
|
||||
"indexed_count": 1,
|
||||
},
|
||||
)
|
||||
self.assertTrue(result["checked_at"])
|
||||
|
||||
def test_fails_when_registry_record_is_missing(self):
|
||||
(self.vault / "_meta" / "source-registry.jsonl").write_text("", encoding="utf-8")
|
||||
|
||||
result = self._verify()
|
||||
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertIn("registry_missing", {item["code"] for item in result["failures"]})
|
||||
|
||||
def test_fails_when_required_archive_is_missing(self):
|
||||
(self.vault / self.archive_path).unlink()
|
||||
|
||||
result = self._verify()
|
||||
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertIn("archive_missing", {item["code"] for item in result["failures"]})
|
||||
|
||||
def test_fails_when_manifest_record_is_missing_or_stale(self):
|
||||
self.manifest_record["ingested_at"] = "2026-07-16T09:59:59Z"
|
||||
manifest = {"version": 1, "sources": {str(self.upload_path): self.manifest_record}}
|
||||
(self.vault / ".manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
||||
|
||||
result = self._verify()
|
||||
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertIn("manifest_missing", {item["code"] for item in result["failures"]})
|
||||
|
||||
def test_fails_when_file_asset_index_row_is_missing(self):
|
||||
with sqlite3.connect(self.vault / "_meta" / "file-assets.sqlite") as conn:
|
||||
conn.execute("DELETE FROM file_assets")
|
||||
|
||||
result = self._verify()
|
||||
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertIn("index_missing", {item["code"] for item in result["failures"]})
|
||||
|
||||
def test_fails_when_declared_output_path_is_missing(self):
|
||||
(self.vault / "references" / "source.md").unlink()
|
||||
|
||||
result = self._verify()
|
||||
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertIn("declared_path_missing", {item["code"] for item in result["failures"]})
|
||||
|
||||
def test_archive_check_is_skipped_when_archiving_is_disabled(self):
|
||||
(self.vault / self.archive_path).unlink()
|
||||
|
||||
result = verify_ingest_outputs(
|
||||
vault_path=self.vault,
|
||||
uploads=[self.upload],
|
||||
job_started_at=self.started_at,
|
||||
archive_sources=False,
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "passed")
|
||||
self.assertEqual(result["outputs"]["archive_count"], 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1055,6 +1055,16 @@ class RemoteMcpServerTests(unittest.TestCase):
|
||||
self.assertEqual(capabilities["ingest"]["preferred_text_tool"], "ingest_text_note")
|
||||
self.assertTrue(capabilities["ingest"]["supports_bundle_upload"])
|
||||
self.assertTrue(capabilities["ingest"]["supports_body_text"])
|
||||
self.assertEqual(
|
||||
capabilities["ingest"]["authoritative_job_phases"],
|
||||
["queued", "preflight", "agent_running", "verifying", "complete"],
|
||||
)
|
||||
self.assertEqual(
|
||||
capabilities["ingest"]["process_alive_source"],
|
||||
"process_supervisor",
|
||||
)
|
||||
self.assertTrue(capabilities["ingest"]["supports_post_run_verification"])
|
||||
self.assertFalse(capabilities["ingest"]["semantic_activity_inference"])
|
||||
self.assertIn("body_text", capabilities["ingest"]["multipart_optional_fields"])
|
||||
self.assertIn("source_type", capabilities["ingest"]["multipart_optional_fields"])
|
||||
self.assertIn("validate_upload", capabilities["ingest"]["tools"])
|
||||
|
||||
Reference in New Issue
Block a user