2026-07-29 14:33:42 -04:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-07-29 15:01:11 -04:00
|
|
|
import asyncio
|
2026-07-29 14:33:42 -04:00
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import shutil
|
2026-07-29 15:01:11 -04:00
|
|
|
import subprocess
|
|
|
|
|
import sys
|
2026-07-29 14:33:42 -04:00
|
|
|
import tempfile
|
|
|
|
|
import tomllib
|
|
|
|
|
import unittest
|
2026-07-29 15:01:11 -04:00
|
|
|
import venv
|
2026-07-29 14:33:42 -04:00
|
|
|
from dataclasses import replace
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
from jsonschema import Draft202012Validator
|
2026-07-29 15:01:11 -04:00
|
|
|
from mcp import ClientSession, StdioServerParameters
|
|
|
|
|
from mcp.client.stdio import stdio_client
|
2026-07-29 14:33:42 -04:00
|
|
|
from referencing import Registry, Resource
|
|
|
|
|
|
2026-07-29 15:47:08 -04:00
|
|
|
from docforge._version import __version__
|
2026-07-29 14:33:42 -04:00
|
|
|
from docforge.adapter_contract import AdapterNode, AdapterProject, AdapterProjection
|
|
|
|
|
from docforge.adapter_launcher import AdapterLauncherV1
|
|
|
|
|
from docforge.client_config import (
|
|
|
|
|
_validate_adapter_configuration_result,
|
|
|
|
|
generate_adapter_client_configuration,
|
|
|
|
|
generate_client_configuration,
|
|
|
|
|
)
|
|
|
|
|
from docforge.errors import DocForgeError
|
|
|
|
|
from docforge.models import Node
|
|
|
|
|
from docforge.project import Project
|
2026-07-29 15:01:11 -04:00
|
|
|
from docforge.reference_mcp import (
|
|
|
|
|
REFERENCE_MCP_MODULE,
|
|
|
|
|
create_reference_project,
|
|
|
|
|
)
|
2026-07-29 14:33:42 -04:00
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
FIXTURES = ROOT / "tests" / "fixtures"
|
|
|
|
|
SCHEMAS = ROOT / "schemas"
|
|
|
|
|
LAUNCHER_SCHEMA = json.loads((SCHEMAS / "adapter-launcher.schema.json").read_text(encoding="utf-8"))
|
|
|
|
|
ADAPTER_CONFIGURATION_SCHEMA = json.loads(
|
|
|
|
|
(SCHEMAS / "adapter-client-configuration.schema.json").read_text(encoding="utf-8")
|
|
|
|
|
)
|
|
|
|
|
GENERIC_CONFIGURATION_SCHEMA = json.loads(
|
|
|
|
|
(SCHEMAS / "client-configuration.schema.json").read_text(encoding="utf-8")
|
|
|
|
|
)
|
|
|
|
|
SCHEMA_REGISTRY = Registry().with_resources(
|
|
|
|
|
(
|
|
|
|
|
(LAUNCHER_SCHEMA["$id"], Resource.from_contents(LAUNCHER_SCHEMA)),
|
|
|
|
|
(
|
|
|
|
|
GENERIC_CONFIGURATION_SCHEMA["$id"],
|
|
|
|
|
Resource.from_contents(GENERIC_CONFIGURATION_SCHEMA),
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
ADAPTER_CONFIGURATION_SCHEMA["$id"],
|
|
|
|
|
Resource.from_contents(ADAPTER_CONFIGURATION_SCHEMA),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
ADAPTER_CONFIGURATION_VALIDATOR = Draft202012Validator(
|
|
|
|
|
ADAPTER_CONFIGURATION_SCHEMA,
|
|
|
|
|
registry=SCHEMA_REGISTRY,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Loader:
|
|
|
|
|
def __init__(self, projection: AdapterProjection) -> None:
|
|
|
|
|
self.projection = projection
|
|
|
|
|
self.load_calls = 0
|
|
|
|
|
|
|
|
|
|
def load_projection(self) -> AdapterProjection:
|
|
|
|
|
self.load_calls += 1
|
|
|
|
|
return self.projection
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DriftingLoader(Loader):
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
projection: AdapterProjection,
|
|
|
|
|
changed: AdapterProjection,
|
|
|
|
|
) -> None:
|
|
|
|
|
super().__init__(projection)
|
|
|
|
|
self.changed = changed
|
|
|
|
|
|
|
|
|
|
def load_projection(self) -> AdapterProjection:
|
|
|
|
|
self.load_calls += 1
|
|
|
|
|
return self.projection if self.load_calls < 3 else self.changed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AdapterLauncherTests(unittest.TestCase):
|
|
|
|
|
def copy_fixture(self, destination: Path) -> Path:
|
|
|
|
|
root = destination / "alpha"
|
|
|
|
|
shutil.copytree(FIXTURES / "alpha", root)
|
|
|
|
|
return root
|
|
|
|
|
|
2026-07-29 15:01:11 -04:00
|
|
|
def copy_reference_fixture(self, destination: Path) -> Path:
|
|
|
|
|
root = destination / "reference-python"
|
|
|
|
|
shutil.copytree(FIXTURES / "reference-python", root)
|
|
|
|
|
config = root / ".docforge" / "reference-adapter.toml"
|
|
|
|
|
config.parent.mkdir(parents=True)
|
|
|
|
|
config.write_text(
|
|
|
|
|
"\n".join(
|
|
|
|
|
(
|
|
|
|
|
"schema_version = 1",
|
|
|
|
|
'project_id = "reference-python"',
|
|
|
|
|
'title = "Runnable Python reference"',
|
|
|
|
|
'language = "python"',
|
|
|
|
|
'source_roots = ["src"]',
|
|
|
|
|
"",
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
return root.resolve()
|
|
|
|
|
|
2026-07-29 14:33:42 -04:00
|
|
|
def projection(
|
|
|
|
|
self,
|
|
|
|
|
root: Path,
|
|
|
|
|
*,
|
|
|
|
|
source_hash: str = "3" * 64,
|
|
|
|
|
) -> AdapterProjection:
|
|
|
|
|
node = Node(
|
|
|
|
|
node_id="source.entry",
|
|
|
|
|
title="Entry",
|
|
|
|
|
family="source",
|
|
|
|
|
authority="derived",
|
|
|
|
|
status="active",
|
|
|
|
|
tags=("python",),
|
|
|
|
|
summary="Reference adapter entry.",
|
|
|
|
|
content="Reference adapter content.",
|
|
|
|
|
source_path="src/entry.py",
|
|
|
|
|
source_anchor="L1",
|
|
|
|
|
content_hash="2" * 64,
|
|
|
|
|
)
|
|
|
|
|
return AdapterProjection(
|
|
|
|
|
project_id="adapter-client-fixture",
|
|
|
|
|
title="Adapter client fixture",
|
|
|
|
|
adapter_id="fixture-client",
|
|
|
|
|
adapter_version="1",
|
|
|
|
|
root=root,
|
|
|
|
|
revision="fixture-revision",
|
|
|
|
|
source_hash=source_hash,
|
|
|
|
|
nodes=(AdapterNode(node),),
|
|
|
|
|
edges=(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def project(
|
|
|
|
|
self,
|
|
|
|
|
root: Path,
|
|
|
|
|
*,
|
|
|
|
|
loader: Loader | None = None,
|
|
|
|
|
) -> tuple[AdapterProject, Loader]:
|
|
|
|
|
effective_loader = loader or Loader(self.projection(root))
|
|
|
|
|
return (
|
|
|
|
|
AdapterProject(
|
|
|
|
|
effective_loader,
|
|
|
|
|
cache_root=root / ".docforge-cache" / "adapter-client",
|
|
|
|
|
),
|
|
|
|
|
effective_loader,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_launcher_and_all_client_fragments_are_deterministic_and_schema_valid(self) -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
|
|
|
root = Path(directory).resolve()
|
|
|
|
|
project, _ = self.project(root)
|
|
|
|
|
launcher = AdapterLauncherV1.for_project(
|
|
|
|
|
project,
|
2026-07-29 15:01:11 -04:00
|
|
|
module=REFERENCE_MCP_MODULE,
|
2026-07-29 14:33:42 -04:00
|
|
|
)
|
|
|
|
|
Draft202012Validator(LAUNCHER_SCHEMA).validate(launcher.as_dict())
|
|
|
|
|
previous = os.environ.get("DOCFORGE_ADAPTER_LAUNCHER_SECRET")
|
|
|
|
|
os.environ["DOCFORGE_ADAPTER_LAUNCHER_SECRET"] = "must-not-appear"
|
|
|
|
|
try:
|
|
|
|
|
with mock.patch("docforge.client_config.subprocess.run") as executed:
|
|
|
|
|
results: dict[str, dict[str, object]] = {}
|
|
|
|
|
for client in ("codex", "claude", "openclaw"):
|
|
|
|
|
first = generate_adapter_client_configuration(
|
|
|
|
|
project,
|
|
|
|
|
launcher,
|
|
|
|
|
client,
|
|
|
|
|
no_ast=True,
|
|
|
|
|
)
|
|
|
|
|
second = generate_adapter_client_configuration(
|
|
|
|
|
project,
|
|
|
|
|
launcher,
|
|
|
|
|
client,
|
|
|
|
|
no_ast=True,
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(first, second)
|
|
|
|
|
ADAPTER_CONFIGURATION_VALIDATOR.validate(first)
|
2026-07-29 15:47:08 -04:00
|
|
|
self.assertEqual(__version__, first["docforge_version"])
|
2026-07-29 14:33:42 -04:00
|
|
|
self.assertEqual(launcher.launcher_hash, first["launcher_hash"])
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
launcher.launcher_hash,
|
|
|
|
|
first["binding"]["launcher_hash"],
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
[
|
|
|
|
|
"-I",
|
|
|
|
|
"-m",
|
2026-07-29 15:01:11 -04:00
|
|
|
REFERENCE_MCP_MODULE,
|
2026-07-29 14:33:42 -04:00
|
|
|
"--project-root",
|
|
|
|
|
str(root),
|
|
|
|
|
"--capability-mode",
|
|
|
|
|
"read",
|
|
|
|
|
"--no-ast",
|
|
|
|
|
],
|
|
|
|
|
first["binding"]["args"],
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual({}, first["binding"]["environment"])
|
|
|
|
|
self.assertNotIn("cwd", first["binding"])
|
|
|
|
|
self.assertNotIn(
|
|
|
|
|
"must-not-appear",
|
|
|
|
|
json.dumps(first, sort_keys=True),
|
|
|
|
|
)
|
|
|
|
|
results[client] = first
|
|
|
|
|
executed.assert_not_called()
|
|
|
|
|
finally:
|
|
|
|
|
if previous is None:
|
|
|
|
|
os.environ.pop("DOCFORGE_ADAPTER_LAUNCHER_SECRET", None)
|
|
|
|
|
else:
|
|
|
|
|
os.environ["DOCFORGE_ADAPTER_LAUNCHER_SECRET"] = previous
|
|
|
|
|
|
|
|
|
|
self.assertIn(
|
|
|
|
|
"mcp_servers",
|
|
|
|
|
tomllib.loads(results["codex"]["artifact"]["content"]),
|
|
|
|
|
)
|
|
|
|
|
self.assertIn(
|
|
|
|
|
"mcpServers",
|
|
|
|
|
json.loads(results["claude"]["artifact"]["content"]),
|
|
|
|
|
)
|
|
|
|
|
self.assertIn(
|
|
|
|
|
"mcp",
|
|
|
|
|
json.loads(results["openclaw"]["artifact"]["content"]),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_launcher_rejects_module_path_entry_point_and_argument_injection(self) -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
|
|
|
root = Path(directory).resolve()
|
|
|
|
|
project, _ = self.project(root)
|
|
|
|
|
launcher = AdapterLauncherV1.for_project(
|
|
|
|
|
project,
|
2026-07-29 15:01:11 -04:00
|
|
|
module=REFERENCE_MCP_MODULE,
|
2026-07-29 14:33:42 -04:00
|
|
|
)
|
|
|
|
|
for module in (
|
|
|
|
|
"-m",
|
2026-07-29 15:01:11 -04:00
|
|
|
"fixture_adapter.mcp_server",
|
2026-07-29 14:33:42 -04:00
|
|
|
"fixture_adapter.mcp_server --debug",
|
|
|
|
|
"fixture_adapter:mcp_server",
|
|
|
|
|
"fixture_adapter/mcp_server",
|
|
|
|
|
".fixture_adapter",
|
|
|
|
|
"docforge.mcp_server",
|
|
|
|
|
"fixture_adapter.$server",
|
|
|
|
|
):
|
|
|
|
|
with self.subTest(module=module), self.assertRaises(DocForgeError) as captured:
|
|
|
|
|
replace(launcher, module=module)
|
|
|
|
|
self.assertEqual("invalid_adapter_launcher", captured.exception.code)
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(DocForgeError):
|
|
|
|
|
replace(launcher, project_root=Path("relative/project"))
|
|
|
|
|
with self.assertRaises(DocForgeError):
|
|
|
|
|
replace(launcher, entry_point="console-script") # type: ignore[arg-type]
|
|
|
|
|
payload = launcher.as_dict()
|
|
|
|
|
payload["arguments"] = ["--shell", "command"]
|
|
|
|
|
with self.assertRaises(TypeError):
|
|
|
|
|
AdapterLauncherV1(**payload) # type: ignore[arg-type]
|
|
|
|
|
|
|
|
|
|
result = generate_adapter_client_configuration(project, launcher, "codex")
|
2026-07-29 15:47:08 -04:00
|
|
|
version_drift = json.loads(json.dumps(result))
|
|
|
|
|
version_drift["docforge_version"] = "0.0.0"
|
|
|
|
|
with self.assertRaisesRegex(AssertionError, "product version"):
|
|
|
|
|
_validate_adapter_configuration_result(
|
|
|
|
|
version_drift,
|
|
|
|
|
project=project,
|
|
|
|
|
launcher=launcher,
|
|
|
|
|
source_availability=project_source_availability(version_drift),
|
|
|
|
|
)
|
2026-07-29 14:33:42 -04:00
|
|
|
result["binding"]["args"].append("--arbitrary")
|
|
|
|
|
with self.assertRaisesRegex(AssertionError, "Generated adapter client"):
|
|
|
|
|
_validate_adapter_configuration_result(
|
|
|
|
|
result,
|
|
|
|
|
project=project,
|
|
|
|
|
launcher=launcher,
|
|
|
|
|
source_availability=project_source_availability(result),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_wrong_project_descriptor_and_source_drift_fail_closed(self) -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
|
|
|
parent = Path(directory)
|
|
|
|
|
first_root = (parent / "first").resolve()
|
|
|
|
|
second_root = (parent / "second").resolve()
|
|
|
|
|
first_root.mkdir()
|
|
|
|
|
second_root.mkdir()
|
|
|
|
|
first, _ = self.project(first_root)
|
|
|
|
|
second, _ = self.project(second_root)
|
|
|
|
|
launcher = AdapterLauncherV1.for_project(
|
|
|
|
|
first,
|
2026-07-29 15:01:11 -04:00
|
|
|
module=REFERENCE_MCP_MODULE,
|
2026-07-29 14:33:42 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with self.assertRaises(DocForgeError) as wrong_project:
|
|
|
|
|
generate_adapter_client_configuration(second, launcher, "codex")
|
|
|
|
|
self.assertEqual("adapter_launcher_mismatch", wrong_project.exception.code)
|
|
|
|
|
with self.assertRaises(DocForgeError) as descriptor_drift:
|
|
|
|
|
generate_adapter_client_configuration(
|
|
|
|
|
first,
|
|
|
|
|
replace(launcher, descriptor_hash="0" * 64),
|
|
|
|
|
"codex",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual("adapter_launcher_mismatch", descriptor_drift.exception.code)
|
|
|
|
|
|
|
|
|
|
base = self.projection(first_root)
|
|
|
|
|
changed = replace(base, source_hash="4" * 64)
|
|
|
|
|
drifting, _ = self.project(
|
|
|
|
|
first_root,
|
|
|
|
|
loader=DriftingLoader(base, changed),
|
|
|
|
|
)
|
|
|
|
|
drifting_launcher = AdapterLauncherV1.for_project(
|
|
|
|
|
drifting,
|
2026-07-29 15:01:11 -04:00
|
|
|
module=REFERENCE_MCP_MODULE,
|
2026-07-29 14:33:42 -04:00
|
|
|
)
|
|
|
|
|
with self.assertRaises(DocForgeError) as source_drift:
|
|
|
|
|
generate_adapter_client_configuration(
|
|
|
|
|
drifting,
|
|
|
|
|
drifting_launcher,
|
|
|
|
|
"openclaw",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual("source_changed", source_drift.exception.code)
|
|
|
|
|
|
|
|
|
|
def test_generic_configuration_remains_separate_and_compatible(self) -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
|
|
|
root = self.copy_fixture(Path(directory))
|
|
|
|
|
generic = Project.open(root)
|
|
|
|
|
result = generate_client_configuration(generic, "codex", no_ast=True)
|
|
|
|
|
Draft202012Validator(GENERIC_CONFIGURATION_SCHEMA).validate(result)
|
|
|
|
|
self.assertEqual("client.configure", result["operation"])
|
|
|
|
|
self.assertNotIn("launcher", result)
|
|
|
|
|
with self.assertRaises(DocForgeError) as generic_launcher:
|
|
|
|
|
AdapterLauncherV1.for_project(
|
|
|
|
|
generic,
|
2026-07-29 15:01:11 -04:00
|
|
|
module=REFERENCE_MCP_MODULE,
|
2026-07-29 14:33:42 -04:00
|
|
|
)
|
|
|
|
|
self.assertEqual("adapter_launcher_unavailable", generic_launcher.exception.code)
|
|
|
|
|
|
|
|
|
|
custom_root = (Path(directory) / "custom").resolve()
|
|
|
|
|
custom_root.mkdir()
|
|
|
|
|
custom, _ = self.project(custom_root)
|
|
|
|
|
with self.assertRaises(DocForgeError) as generic_api:
|
|
|
|
|
generate_client_configuration(custom, "codex")
|
|
|
|
|
self.assertEqual("missing_config", generic_api.exception.code)
|
|
|
|
|
|
2026-07-29 15:01:11 -04:00
|
|
|
def test_installed_project_owned_top_level_module_launches_without_probe_execution(
|
|
|
|
|
self,
|
|
|
|
|
) -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
|
|
|
root = Path(directory).resolve()
|
|
|
|
|
environment = root / ".launcher-venv"
|
|
|
|
|
venv.EnvBuilder(with_pip=False).create(environment)
|
|
|
|
|
executable = environment / "bin" / "python"
|
|
|
|
|
site_packages = Path(
|
|
|
|
|
subprocess.run(
|
|
|
|
|
[
|
|
|
|
|
str(executable),
|
|
|
|
|
"-I",
|
|
|
|
|
"-c",
|
|
|
|
|
"import site; print(site.getsitepackages()[0])",
|
|
|
|
|
],
|
|
|
|
|
check=True,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
).stdout.strip()
|
|
|
|
|
)
|
|
|
|
|
(site_packages / "fixture-adapter.pth").write_text(
|
|
|
|
|
f"{root}\n",
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
sentinel = root / "fixture-adapter-executed"
|
|
|
|
|
(root / "fixture_adapter.py").write_text(
|
|
|
|
|
"\n".join(
|
|
|
|
|
(
|
|
|
|
|
"import argparse",
|
|
|
|
|
"import json",
|
|
|
|
|
"from pathlib import Path",
|
|
|
|
|
"parser = argparse.ArgumentParser()",
|
|
|
|
|
'parser.add_argument("--project-root", required=True)',
|
|
|
|
|
(
|
|
|
|
|
'parser.add_argument("--capability-mode", '
|
|
|
|
|
'choices=("read",), required=True)'
|
|
|
|
|
),
|
|
|
|
|
"arguments = parser.parse_args()",
|
|
|
|
|
f"Path({str(sentinel)!r}).write_text('executed', encoding='utf-8')",
|
|
|
|
|
(
|
|
|
|
|
"print(json.dumps({'project_root': arguments.project_root}, "
|
|
|
|
|
"sort_keys=True))"
|
|
|
|
|
),
|
|
|
|
|
"",
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
project, _ = self.project(root)
|
|
|
|
|
with mock.patch.object(sys, "executable", str(executable)):
|
|
|
|
|
launcher = AdapterLauncherV1.for_project(
|
|
|
|
|
project,
|
|
|
|
|
module="fixture_adapter",
|
|
|
|
|
)
|
|
|
|
|
result = generate_adapter_client_configuration(
|
|
|
|
|
project,
|
|
|
|
|
launcher,
|
|
|
|
|
"codex",
|
|
|
|
|
)
|
|
|
|
|
self.assertFalse(sentinel.exists())
|
|
|
|
|
Draft202012Validator(LAUNCHER_SCHEMA).validate(launcher.as_dict())
|
|
|
|
|
binding = result["binding"]
|
|
|
|
|
completed = subprocess.run(
|
|
|
|
|
[binding["command"], *binding["args"]],
|
|
|
|
|
check=True,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
timeout=10,
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual({"project_root": str(root)}, json.loads(completed.stdout))
|
|
|
|
|
self.assertEqual("executed", sentinel.read_text(encoding="utf-8"))
|
|
|
|
|
|
|
|
|
|
def test_uninstalled_packages_and_non_project_modules_fail_closed(self) -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
|
|
|
root = Path(directory).resolve()
|
|
|
|
|
project, _ = self.project(root)
|
|
|
|
|
for module, code in (
|
|
|
|
|
("fixture_adapter", "adapter_launcher_unavailable"),
|
|
|
|
|
("os", "adapter_launcher_unavailable"),
|
|
|
|
|
("json", "invalid_adapter_launcher"),
|
|
|
|
|
):
|
|
|
|
|
with self.subTest(module=module), self.assertRaises(DocForgeError) as captured:
|
|
|
|
|
AdapterLauncherV1.for_project(project, module=module)
|
|
|
|
|
self.assertEqual(code, captured.exception.code)
|
|
|
|
|
|
|
|
|
|
def test_generated_reference_binding_starts_a_real_isolated_mcp_process(self) -> None:
|
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
|
|
|
root = self.copy_reference_fixture(Path(directory))
|
|
|
|
|
project = create_reference_project(root)
|
|
|
|
|
launcher = AdapterLauncherV1.for_project(
|
|
|
|
|
project,
|
|
|
|
|
module=REFERENCE_MCP_MODULE,
|
|
|
|
|
)
|
|
|
|
|
result = generate_adapter_client_configuration(
|
|
|
|
|
project,
|
|
|
|
|
launcher,
|
|
|
|
|
"codex",
|
|
|
|
|
)
|
|
|
|
|
binding = result["binding"]
|
|
|
|
|
|
|
|
|
|
async def inspect() -> tuple[str, ...]:
|
|
|
|
|
parameters = StdioServerParameters(
|
|
|
|
|
command=binding["command"],
|
|
|
|
|
args=binding["args"],
|
|
|
|
|
env=binding["environment"],
|
|
|
|
|
)
|
|
|
|
|
async with (
|
|
|
|
|
stdio_client(parameters) as streams,
|
|
|
|
|
ClientSession(*streams) as session,
|
|
|
|
|
):
|
|
|
|
|
await session.initialize()
|
|
|
|
|
return tuple(tool.name for tool in (await session.list_tools()).tools)
|
|
|
|
|
|
|
|
|
|
tools = asyncio.run(inspect())
|
|
|
|
|
self.assertIn("docforge_bootstrap", tools)
|
|
|
|
|
|
2026-07-29 14:33:42 -04:00
|
|
|
|
|
|
|
|
def project_source_availability(result: dict[str, object]):
|
|
|
|
|
from docforge.adapter_launcher import AdapterSourceAvailabilityV1
|
|
|
|
|
|
|
|
|
|
payload = result["source_availability"]
|
|
|
|
|
return AdapterSourceAvailabilityV1(
|
|
|
|
|
schema_version=payload["schema_version"],
|
|
|
|
|
status=payload["status"],
|
|
|
|
|
method=payload["method"],
|
|
|
|
|
revision=payload["revision"],
|
|
|
|
|
source_hash=payload["source_hash"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|