1264 lines
54 KiB
Python
1264 lines
54 KiB
Python
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import shutil
|
|
import stat
|
|
import tempfile
|
|
import tomllib
|
|
import unittest
|
|
from collections.abc import Callable
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
from jsonschema import Draft202012Validator
|
|
from mcp import ClientSession, StdioServerParameters
|
|
from mcp.client.stdio import stdio_client
|
|
|
|
from docforge.cli import _parser, _run, main
|
|
from docforge.client_config import (
|
|
_read_existing,
|
|
_validate_configuration_result,
|
|
generate_client_configuration,
|
|
)
|
|
from docforge.doctor import run_doctor
|
|
from docforge.errors import DocForgeError
|
|
from docforge.index import ProjectIndex
|
|
from docforge.mcp_server import READ_TOOLS
|
|
from docforge.project import MAX_PROJECT_DESCRIPTOR_BYTES, Project
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
FIXTURES = ROOT / "tests" / "fixtures"
|
|
SCHEMAS = ROOT / "schemas"
|
|
CONFIGURATION_SCHEMA = json.loads(
|
|
(SCHEMAS / "client-configuration.schema.json").read_text(encoding="utf-8")
|
|
)
|
|
DOCTOR_SCHEMA = json.loads((SCHEMAS / "doctor-result.schema.json").read_text(encoding="utf-8"))
|
|
POLICY_SCHEMA = json.loads((SCHEMAS / "policy.schema.json").read_text(encoding="utf-8"))
|
|
|
|
|
|
class ClientIntegrationTests(unittest.TestCase):
|
|
def copy_fixture(self, destination: Path) -> Path:
|
|
root = destination / "alpha"
|
|
shutil.copytree(FIXTURES / "alpha", root)
|
|
return root
|
|
|
|
@staticmethod
|
|
def tree_snapshot(root: Path) -> dict[str, tuple[int, int, str]]:
|
|
return {
|
|
path.relative_to(root).as_posix(): (
|
|
path.stat().st_mode,
|
|
path.stat().st_size,
|
|
path.read_bytes().hex(),
|
|
)
|
|
for path in sorted(root.rglob("*"))
|
|
if path.is_file()
|
|
}
|
|
|
|
def test_exact_outline_cli_forms_and_legacy_project_root_forms_parse(self) -> None:
|
|
parser = _parser()
|
|
for client in ("codex", "claude", "openclaw"):
|
|
configured = parser.parse_args(["configure", client, "--project", "/tmp/project"])
|
|
self.assertEqual("configure", configured.command)
|
|
self.assertEqual(client, configured.client)
|
|
self.assertEqual(Path("/tmp/project"), configured.project)
|
|
doctor = parser.parse_args(["doctor", "--client", "codex"])
|
|
self.assertEqual("doctor", doctor.command)
|
|
self.assertIsNone(doctor.project)
|
|
legacy = parser.parse_args(["--project-root", "/tmp/project", "info"])
|
|
self.assertEqual(Path("/tmp/project"), legacy.project_root)
|
|
with self.assertRaises(DocForgeError) as missing:
|
|
_run(parser.parse_args(["info"]))
|
|
self.assertEqual("missing_project_root", missing.exception.code)
|
|
|
|
def test_configuration_preview_is_deterministic_parseable_and_secret_free(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = self.copy_fixture(Path(directory))
|
|
project = Project.open(root)
|
|
before = self.tree_snapshot(root)
|
|
previous = os.environ.get("DOCFORGE_TEST_SECRET")
|
|
os.environ["DOCFORGE_TEST_SECRET"] = "must-not-appear"
|
|
try:
|
|
results: dict[str, dict[str, object]] = {}
|
|
for client in ("codex", "claude", "openclaw"):
|
|
first = generate_client_configuration(project, client, no_ast=True)
|
|
second = generate_client_configuration(project, client, no_ast=True)
|
|
self.assertEqual(first, second)
|
|
Draft202012Validator(CONFIGURATION_SCHEMA).validate(first)
|
|
Draft202012Validator(POLICY_SCHEMA).validate(first["effective_policy"])
|
|
self.assertEqual("read", first["binding"]["capability_mode"])
|
|
self.assertEqual(
|
|
"preserve-no-ast",
|
|
first["binding"]["adapter_policy"]["mode"],
|
|
)
|
|
self.assertEqual({}, first["binding"]["environment"])
|
|
self.assertNotIn(
|
|
"must-not-appear",
|
|
json.dumps(first, sort_keys=True),
|
|
)
|
|
results[client] = first
|
|
finally:
|
|
if previous is None:
|
|
os.environ.pop("DOCFORGE_TEST_SECRET", None)
|
|
else:
|
|
os.environ["DOCFORGE_TEST_SECRET"] = previous
|
|
|
|
codex_content = results["codex"]["artifact"]["content"]
|
|
self.assertIn("mcp_servers", tomllib.loads(codex_content))
|
|
openclaw_content = results["openclaw"]["artifact"]["content"]
|
|
self.assertIn("mcp", json.loads(openclaw_content))
|
|
claude_content = results["claude"]["artifact"]["content"]
|
|
self.assertIn("mcpServers", json.loads(claude_content))
|
|
self.assertEqual(before, self.tree_snapshot(root))
|
|
|
|
def test_capability_bindings_fail_closed_and_render_policy_is_derived(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
project = Project.open(self.copy_fixture(Path(directory)))
|
|
with self.assertRaises(DocForgeError) as missing_writer:
|
|
generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
capability_mode="proposal",
|
|
)
|
|
self.assertEqual("capability_unavailable", missing_writer.exception.code)
|
|
proposal = generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
capability_mode="proposal",
|
|
proposal_writer="alpha-editor",
|
|
)
|
|
self.assertEqual("explicit", proposal["binding"]["render_policy"]["manual"])
|
|
application = generate_client_configuration(
|
|
project,
|
|
"openclaw",
|
|
capability_mode="application",
|
|
proposal_writer="alpha-editor",
|
|
canonical_applier="alpha-editor",
|
|
)
|
|
self.assertEqual("auto", application["binding"]["render_policy"]["manual"])
|
|
with self.assertRaises(DocForgeError) as mismatch:
|
|
generate_client_configuration(
|
|
project,
|
|
"openclaw",
|
|
capability_mode="application",
|
|
proposal_writer="alpha-editor",
|
|
canonical_applier="other",
|
|
)
|
|
self.assertEqual("capability_unavailable", mismatch.exception.code)
|
|
with self.assertRaises(DocForgeError) as escalated:
|
|
generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
proposal_writer="alpha-editor",
|
|
)
|
|
self.assertEqual("invalid_capability_binding", escalated.exception.code)
|
|
|
|
def test_explicit_fragment_write_is_atomic_conflict_aware_and_private(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
project = Project.open(self.copy_fixture(parent))
|
|
output = parent / "client" / "docforge.toml"
|
|
output.parent.mkdir()
|
|
created = generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=output,
|
|
)
|
|
self.assertEqual("created", created["artifact"]["write_state"])
|
|
self.assertEqual(
|
|
stat.S_IMODE(output.stat().st_mode),
|
|
0o600,
|
|
)
|
|
unchanged = generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=output,
|
|
)
|
|
self.assertEqual("unchanged", unchanged["artifact"]["write_state"])
|
|
|
|
output.write_text("different\n", encoding="utf-8")
|
|
with self.assertRaises(DocForgeError) as conflict:
|
|
generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=output,
|
|
)
|
|
self.assertEqual("output_conflict", conflict.exception.code)
|
|
self.assertEqual("different\n", output.read_text(encoding="utf-8"))
|
|
output.unlink()
|
|
output.symlink_to(parent / "outside")
|
|
with self.assertRaises(DocForgeError) as unsafe:
|
|
generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=output,
|
|
)
|
|
self.assertEqual("unsafe_output", unsafe.exception.code)
|
|
self.assertEqual([], list(output.parent.glob(".docforge-client-*")))
|
|
|
|
linked_parent = parent / "linked-client"
|
|
outside = parent / "outside-client"
|
|
outside.mkdir()
|
|
linked_parent.symlink_to(outside, target_is_directory=True)
|
|
with self.assertRaises(DocForgeError) as escaped:
|
|
generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=linked_parent / "fragment.toml",
|
|
)
|
|
self.assertEqual("unsafe_output", escaped.exception.code)
|
|
self.assertFalse((outside / "fragment.toml").exists())
|
|
|
|
def test_committed_fragment_reports_unconfirmed_directory_durability(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
project = Project.open(self.copy_fixture(parent))
|
|
output = parent / "client" / "docforge.toml"
|
|
output.parent.mkdir()
|
|
with mock.patch(
|
|
"docforge.client_config.os.fsync",
|
|
side_effect=[None, OSError("directory fsync unavailable")],
|
|
):
|
|
created = generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=output,
|
|
)
|
|
Draft202012Validator(CONFIGURATION_SCHEMA).validate(created)
|
|
self.assertEqual("created", created["artifact"]["write_state"])
|
|
self.assertEqual("unconfirmed", created["artifact"]["durability"])
|
|
self.assertEqual(
|
|
[{"code": "publication_durability_unconfirmed"}],
|
|
created["warnings"],
|
|
)
|
|
self.assertTrue(output.is_file())
|
|
self.assertEqual(
|
|
created["artifact"]["content"],
|
|
output.read_text(encoding="utf-8"),
|
|
)
|
|
unchanged = generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=output,
|
|
)
|
|
self.assertEqual("unchanged", unchanged["artifact"]["write_state"])
|
|
|
|
def test_configuration_fails_closed_on_stale_or_unimportable_runtime(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = self.copy_fixture(Path(directory))
|
|
project = Project.open(root)
|
|
descriptor = root / ".docforge" / "project.toml"
|
|
descriptor.write_text(
|
|
descriptor.read_text(encoding="utf-8") + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
with self.assertRaises(DocForgeError) as stale:
|
|
generate_client_configuration(project, "codex")
|
|
self.assertEqual("source_changed", stale.exception.code)
|
|
|
|
current = Project.open(root)
|
|
failed_probe = mock.Mock(returncode=1)
|
|
with (
|
|
mock.patch(
|
|
"docforge.client_config.subprocess.run",
|
|
return_value=failed_probe,
|
|
),
|
|
self.assertRaises(DocForgeError) as unavailable,
|
|
):
|
|
generate_client_configuration(current, "codex")
|
|
self.assertEqual(
|
|
"client_configuration_unavailable",
|
|
unavailable.exception.code,
|
|
)
|
|
|
|
def test_fragment_publication_rejects_parent_replacement_and_os_errors(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
project = Project.open(self.copy_fixture(parent))
|
|
output_parent = parent / "client"
|
|
output_parent.mkdir()
|
|
output = output_parent / "docforge.toml"
|
|
moved = parent / "moved-client"
|
|
original_link = os.link
|
|
swapped = False
|
|
|
|
def swap_parent(*args: object, **kwargs: object) -> None:
|
|
nonlocal swapped
|
|
if not swapped:
|
|
output_parent.rename(moved)
|
|
output_parent.mkdir()
|
|
swapped = True
|
|
original_link(*args, **kwargs)
|
|
|
|
with (
|
|
mock.patch(
|
|
"docforge.client_config.os.link",
|
|
side_effect=swap_parent,
|
|
),
|
|
self.assertRaises(DocForgeError) as changed,
|
|
):
|
|
generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=output,
|
|
)
|
|
self.assertEqual("output_changed", changed.exception.code)
|
|
self.assertFalse(output.exists())
|
|
self.assertEqual([], list(moved.glob("docforge.toml")))
|
|
self.assertEqual([], list(moved.glob(".docforge-client-*")))
|
|
|
|
with (
|
|
mock.patch(
|
|
"docforge.client_config.os.link",
|
|
side_effect=PermissionError("denied"),
|
|
),
|
|
self.assertRaises(DocForgeError) as denied,
|
|
):
|
|
generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=output,
|
|
)
|
|
self.assertEqual("output_publication_failed", denied.exception.code)
|
|
self.assertEqual([], list(output_parent.glob(".docforge-client-*")))
|
|
|
|
def test_identical_existing_fragment_must_already_be_private_and_singly_linked(
|
|
self,
|
|
) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
project = Project.open(self.copy_fixture(parent))
|
|
output = parent / "docforge.toml"
|
|
preview = generate_client_configuration(project, "codex")
|
|
output.write_text(preview["artifact"]["content"], encoding="utf-8")
|
|
output.chmod(0o644)
|
|
with self.assertRaises(DocForgeError) as public:
|
|
generate_client_configuration(project, "codex", output=output)
|
|
self.assertEqual("unsafe_output", public.exception.code)
|
|
|
|
def test_unchanged_fragment_revalidates_binding_after_read(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
output = parent / "docforge.toml"
|
|
generate_client_configuration(project, "codex", output=output)
|
|
descriptor = root / ".docforge" / "project.toml"
|
|
|
|
def mutate_after_read(*args: object, **kwargs: object) -> bytes:
|
|
content = _read_existing(*args, **kwargs) # type: ignore[arg-type]
|
|
descriptor.write_text(
|
|
descriptor.read_text(encoding="utf-8") + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
return content
|
|
|
|
with (
|
|
mock.patch(
|
|
"docforge.client_config._read_existing",
|
|
side_effect=mutate_after_read,
|
|
),
|
|
self.assertRaises(DocForgeError) as stale,
|
|
):
|
|
generate_client_configuration(project, "codex", output=output)
|
|
self.assertEqual("source_changed", stale.exception.code)
|
|
|
|
def test_fragment_publication_revalidates_descriptor_after_link(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
output_parent = parent / "client"
|
|
output_parent.mkdir()
|
|
output = output_parent / "docforge.toml"
|
|
descriptor = root / ".docforge" / "project.toml"
|
|
original_link = os.link
|
|
|
|
def mutate_descriptor(*args: object, **kwargs: object) -> None:
|
|
original_link(*args, **kwargs)
|
|
descriptor.write_text(
|
|
descriptor.read_text(encoding="utf-8") + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with (
|
|
mock.patch(
|
|
"docforge.client_config.os.link",
|
|
side_effect=mutate_descriptor,
|
|
),
|
|
self.assertRaises(DocForgeError) as stale,
|
|
):
|
|
generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=output,
|
|
)
|
|
self.assertEqual("source_changed", stale.exception.code)
|
|
self.assertFalse(output.exists())
|
|
self.assertEqual([], list(output_parent.glob(".docforge-client-*")))
|
|
|
|
def test_unprovable_postlink_binding_returns_schema_valid_degraded_evidence(
|
|
self,
|
|
) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
output_parent = parent / "client"
|
|
output_parent.mkdir()
|
|
output = output_parent / "docforge.toml"
|
|
descriptor = root / ".docforge" / "project.toml"
|
|
original_link = os.link
|
|
|
|
def mutate_descriptor(*args: object, **kwargs: object) -> None:
|
|
original_link(*args, **kwargs)
|
|
descriptor.write_text(
|
|
descriptor.read_text(encoding="utf-8") + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with (
|
|
mock.patch(
|
|
"docforge.client_config.os.link",
|
|
side_effect=mutate_descriptor,
|
|
),
|
|
mock.patch(
|
|
"docforge.client_config._rollback_link",
|
|
return_value=False,
|
|
),
|
|
):
|
|
result = generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=output,
|
|
)
|
|
Draft202012Validator(CONFIGURATION_SCHEMA).validate(result)
|
|
self.assertEqual("created", result["artifact"]["write_state"])
|
|
self.assertEqual("unconfirmed", result["artifact"]["durability"])
|
|
self.assertIsNone(result["artifact"]["output_path"])
|
|
self.assertIn(
|
|
{"code": "publication_binding_unconfirmed"},
|
|
result["warnings"],
|
|
)
|
|
|
|
def test_fragment_publication_revalidates_after_directory_fsync(self) -> None:
|
|
for scenario in ("descriptor", "target", "parent"):
|
|
with self.subTest(scenario=scenario), tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
output_parent = parent / "client"
|
|
output_parent.mkdir()
|
|
output = output_parent / "docforge.toml"
|
|
descriptor = root / ".docforge" / "project.toml"
|
|
moved = parent / "moved-client"
|
|
original_fsync = os.fsync
|
|
calls = 0
|
|
|
|
def mutate_at_directory_fsync(
|
|
file_descriptor: int,
|
|
selected_scenario: str = scenario,
|
|
selected_descriptor: Path = descriptor,
|
|
selected_output: Path = output,
|
|
selected_parent: Path = output_parent,
|
|
selected_moved: Path = moved,
|
|
selected_fsync: Callable[[int], None] = original_fsync,
|
|
) -> None:
|
|
nonlocal calls
|
|
calls += 1
|
|
selected_fsync(file_descriptor)
|
|
if calls != 2:
|
|
return
|
|
if selected_scenario == "descriptor":
|
|
selected_descriptor.write_text(
|
|
selected_descriptor.read_text(encoding="utf-8") + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
elif selected_scenario == "target":
|
|
selected_output.write_text("tampered\n", encoding="utf-8")
|
|
else:
|
|
selected_parent.rename(selected_moved)
|
|
selected_parent.mkdir()
|
|
|
|
with (
|
|
mock.patch(
|
|
"docforge.client_config.os.fsync",
|
|
side_effect=mutate_at_directory_fsync,
|
|
),
|
|
self.assertRaises(DocForgeError) as changed,
|
|
):
|
|
generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
output=output,
|
|
)
|
|
self.assertIn(changed.exception.code, {"source_changed", "output_changed"})
|
|
self.assertFalse(output.exists())
|
|
self.assertFalse((moved / "docforge.toml").exists())
|
|
|
|
def test_doctor_round_trip_is_schema_valid_and_performs_no_hidden_work(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
ProjectIndex(project).build()
|
|
config = parent / "codex.toml"
|
|
generated = generate_client_configuration(
|
|
project,
|
|
"codex",
|
|
no_ast=True,
|
|
output=config,
|
|
)
|
|
before_project = self.tree_snapshot(root)
|
|
before_config = config.read_bytes()
|
|
with (
|
|
mock.patch.object(
|
|
Project,
|
|
"load",
|
|
side_effect=AssertionError("doctor must not load project sources"),
|
|
),
|
|
mock.patch(
|
|
"docforge.index.ProjectIndex.check",
|
|
side_effect=AssertionError("doctor must not check SQLite"),
|
|
),
|
|
mock.patch(
|
|
"docforge.index.ProjectIndex.synchronize",
|
|
side_effect=AssertionError("doctor must not synchronize"),
|
|
),
|
|
mock.patch(
|
|
"docforge.index.ProjectIndex.build",
|
|
side_effect=AssertionError("doctor must not build"),
|
|
),
|
|
mock.patch(
|
|
"subprocess.run",
|
|
side_effect=AssertionError("doctor must not execute configured commands"),
|
|
),
|
|
):
|
|
result = run_doctor(
|
|
project,
|
|
"codex",
|
|
config_path=config,
|
|
server_name=generated["server_name"],
|
|
)
|
|
Draft202012Validator(DOCTOR_SCHEMA).validate(result)
|
|
self.assertEqual("healthy", result["doctor_state"])
|
|
self.assertEqual(0, result["summary"]["warning"])
|
|
self.assertEqual(0, result["summary"]["failed"])
|
|
self.assertTrue(result["guarantees"]["read_only"])
|
|
no_ast = next(
|
|
check for check in result["checks"] if check["check_id"] == "policy.no_ast"
|
|
)
|
|
self.assertEqual("no_ast_policy_valid", no_ast["code"])
|
|
self.assertEqual("unverifiable", no_ast["details"]["adapter_internals"])
|
|
self.assertEqual(before_project, self.tree_snapshot(root))
|
|
self.assertEqual(before_config, config.read_bytes())
|
|
|
|
def test_doctor_reports_missing_malformed_and_secret_environment_without_echoing(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
missing = run_doctor(
|
|
project,
|
|
"codex",
|
|
config_path=parent / "missing.toml",
|
|
)
|
|
Draft202012Validator(DOCTOR_SCHEMA).validate(missing)
|
|
self.assertEqual("unhealthy", missing["doctor_state"])
|
|
malformed_path = parent / "malformed.toml"
|
|
malformed_path.write_text("[invalid", encoding="utf-8")
|
|
malformed = run_doctor(
|
|
project,
|
|
"codex",
|
|
config_path=malformed_path,
|
|
)
|
|
Draft202012Validator(DOCTOR_SCHEMA).validate(malformed)
|
|
self.assertEqual("unhealthy", malformed["doctor_state"])
|
|
|
|
generated = generate_client_configuration(project, "openclaw")
|
|
document = json.loads(generated["artifact"]["content"])
|
|
entry = next(iter(document["mcp"]["servers"].values()))
|
|
entry["env"] = {"TOKEN": "top-secret-value"}
|
|
secret_path = parent / "openclaw.json"
|
|
secret_path.write_text(
|
|
json.dumps(document, sort_keys=True),
|
|
encoding="utf-8",
|
|
)
|
|
secret = run_doctor(
|
|
project,
|
|
"openclaw",
|
|
config_path=secret_path,
|
|
server_name=generated["server_name"],
|
|
)
|
|
Draft202012Validator(DOCTOR_SCHEMA).validate(secret)
|
|
self.assertEqual("degraded", secret["doctor_state"])
|
|
encoded = json.dumps(secret, sort_keys=True)
|
|
self.assertIn("TOKEN", encoded)
|
|
self.assertNotIn("top-secret-value", encoded)
|
|
|
|
def test_cli_doctor_exit_codes_are_stable(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
ProjectIndex(project).build()
|
|
config = parent / "codex.toml"
|
|
generated = generate_client_configuration(project, "codex", output=config)
|
|
output = io.StringIO()
|
|
with contextlib.redirect_stdout(output):
|
|
healthy = main(
|
|
[
|
|
"doctor",
|
|
"--client",
|
|
"codex",
|
|
"--project",
|
|
str(root),
|
|
"--config",
|
|
str(config),
|
|
"--server-name",
|
|
str(generated["server_name"]),
|
|
]
|
|
)
|
|
self.assertEqual(0, healthy)
|
|
self.assertEqual("healthy", json.loads(output.getvalue())["doctor_state"])
|
|
|
|
output = io.StringIO()
|
|
with contextlib.redirect_stdout(output):
|
|
degraded = main(
|
|
[
|
|
"doctor",
|
|
"--client",
|
|
"codex",
|
|
"--project",
|
|
str(root),
|
|
"--config",
|
|
str(parent / "missing.toml"),
|
|
]
|
|
)
|
|
self.assertEqual(2, degraded)
|
|
self.assertEqual("unhealthy", json.loads(output.getvalue())["doctor_state"])
|
|
|
|
malformed = parent / "malformed.toml"
|
|
malformed.write_text("[invalid", encoding="utf-8")
|
|
output = io.StringIO()
|
|
with contextlib.redirect_stdout(output):
|
|
unhealthy = main(
|
|
[
|
|
"doctor",
|
|
"--client",
|
|
"codex",
|
|
"--project",
|
|
str(root),
|
|
"--config",
|
|
str(malformed),
|
|
]
|
|
)
|
|
self.assertEqual(2, unhealthy)
|
|
self.assertEqual("unhealthy", json.loads(output.getvalue())["doctor_state"])
|
|
|
|
def test_project_descriptor_read_is_bounded_stable_and_symlink_safe(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
descriptor = root / ".docforge" / "project.toml"
|
|
original = descriptor.read_bytes()
|
|
descriptor.unlink()
|
|
external = parent / "external.toml"
|
|
external.write_bytes(original)
|
|
descriptor.symlink_to(external)
|
|
with self.assertRaises(DocForgeError) as symlinked:
|
|
Project.open(root)
|
|
self.assertEqual("project_descriptor_unsafe", symlinked.exception.code)
|
|
|
|
descriptor.unlink()
|
|
descriptor.write_bytes(b"x" * (MAX_PROJECT_DESCRIPTOR_BYTES + 1))
|
|
with self.assertRaises(DocForgeError) as oversized:
|
|
Project.open(root)
|
|
self.assertEqual("project_descriptor_oversized", oversized.exception.code)
|
|
|
|
shutil.rmtree(root / ".docforge")
|
|
external_directory = parent / "external-docforge"
|
|
external_directory.mkdir()
|
|
(external_directory / "project.toml").write_bytes(original)
|
|
(root / ".docforge").symlink_to(external_directory, target_is_directory=True)
|
|
with self.assertRaises(DocForgeError) as escaped:
|
|
Project.open(root)
|
|
self.assertEqual("project_descriptor_unsafe", escaped.exception.code)
|
|
|
|
def test_project_load_revalidates_the_bounded_descriptor(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = self.copy_fixture(Path(directory))
|
|
project = Project.open(root)
|
|
descriptor = root / ".docforge" / "project.toml"
|
|
descriptor.write_bytes(b"x" * (MAX_PROJECT_DESCRIPTOR_BYTES + 1))
|
|
with self.assertRaises(DocForgeError) as oversized:
|
|
project.load()
|
|
self.assertEqual("project_descriptor_oversized", oversized.exception.code)
|
|
|
|
def test_doctor_ignores_unrelated_entries_and_honors_codex_home(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
ProjectIndex(project).build()
|
|
codex_home = parent / "codex-home"
|
|
codex_home.mkdir()
|
|
config = codex_home / "config.toml"
|
|
generated = generate_client_configuration(project, "codex", output=config)
|
|
config.write_text(
|
|
config.read_text(encoding="utf-8")
|
|
+ '\n[mcp_servers."remote"]\nurl = "https://example.invalid/mcp"\n',
|
|
encoding="utf-8",
|
|
)
|
|
with mock.patch.dict(os.environ, {"CODEX_HOME": str(codex_home)}):
|
|
result = run_doctor(project, "codex")
|
|
Draft202012Validator(DOCTOR_SCHEMA).validate(result)
|
|
self.assertEqual("healthy", result["doctor_state"])
|
|
self.assertEqual(generated["server_name"], result["config"]["server_name"])
|
|
|
|
openclaw_config = parent / "openclaw.json"
|
|
openclaw = generate_client_configuration(
|
|
project,
|
|
"openclaw",
|
|
output=openclaw_config,
|
|
)
|
|
document = json.loads(openclaw_config.read_text(encoding="utf-8"))
|
|
document["mcp"]["servers"]["remote"] = {
|
|
"type": "http",
|
|
"url": "https://example.invalid/mcp",
|
|
"enabled": True,
|
|
}
|
|
openclaw_config.write_text(
|
|
json.dumps(document, sort_keys=True),
|
|
encoding="utf-8",
|
|
)
|
|
openclaw_result = run_doctor(
|
|
project,
|
|
"openclaw",
|
|
config_path=openclaw_config,
|
|
)
|
|
Draft202012Validator(DOCTOR_SCHEMA).validate(openclaw_result)
|
|
self.assertEqual("healthy", openclaw_result["doctor_state"])
|
|
self.assertEqual(
|
|
openclaw["server_name"],
|
|
openclaw_result["config"]["server_name"],
|
|
)
|
|
|
|
selected = next(iter(document["mcp"]["servers"].values()))
|
|
selected["cwd"] = str(root)
|
|
selected["toolFilter"] = {"include": ["docforge_bootstrap"]}
|
|
openclaw_config.write_text(
|
|
json.dumps(document, sort_keys=True),
|
|
encoding="utf-8",
|
|
)
|
|
filtered = run_doctor(
|
|
project,
|
|
"openclaw",
|
|
config_path=openclaw_config,
|
|
)
|
|
Draft202012Validator(DOCTOR_SCHEMA).validate(filtered)
|
|
self.assertEqual("degraded", filtered["doctor_state"])
|
|
self.assertIn(
|
|
"client_tool_filter_unverified",
|
|
[check["code"] for check in filtered["checks"]],
|
|
)
|
|
|
|
def test_doctor_rejects_nonlaunching_or_malformed_matching_arguments(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
ProjectIndex(project).build()
|
|
generated = generate_client_configuration(project, "openclaw")
|
|
document = json.loads(generated["artifact"]["content"])
|
|
entry = next(iter(document["mcp"]["servers"].values()))
|
|
entry["args"] = entry["args"][3:]
|
|
config = parent / "missing-prefix.json"
|
|
config.write_text(json.dumps(document), encoding="utf-8")
|
|
missing_prefix = run_doctor(
|
|
project,
|
|
"openclaw",
|
|
config_path=config,
|
|
)
|
|
self.assertEqual("unhealthy", missing_prefix["doctor_state"])
|
|
self.assertIn(
|
|
"server_arguments_invalid",
|
|
[check["code"] for check in missing_prefix["checks"]],
|
|
)
|
|
|
|
entry["args"] = [
|
|
"-I",
|
|
"-m",
|
|
"docforge.mcp_server",
|
|
"--project-root",
|
|
str(root),
|
|
"--bogus",
|
|
]
|
|
config.write_text(json.dumps(document), encoding="utf-8")
|
|
malformed = run_doctor(project, "openclaw", config_path=config)
|
|
self.assertEqual("unhealthy", malformed["doctor_state"])
|
|
self.assertIn(
|
|
"server_arguments_invalid",
|
|
[check["code"] for check in malformed["checks"]],
|
|
)
|
|
|
|
secret = "super-secret-argument"
|
|
entry["args"][-1] = secret
|
|
config.write_text(json.dumps(document), encoding="utf-8")
|
|
redacted = run_doctor(project, "openclaw", config_path=config)
|
|
self.assertEqual("unhealthy", redacted["doctor_state"])
|
|
self.assertNotIn(secret, json.dumps(redacted, sort_keys=True))
|
|
|
|
def test_doctor_rejects_ambiguous_duplicate_roots_and_malformed_controls(
|
|
self,
|
|
) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
ProjectIndex(project).build()
|
|
generated = generate_client_configuration(project, "openclaw")
|
|
document = json.loads(generated["artifact"]["content"])
|
|
name, first = next(iter(document["mcp"]["servers"].items()))
|
|
duplicate = json.loads(json.dumps(first))
|
|
root_value = duplicate["args"].index("--project-root") + 1
|
|
duplicate["args"][root_value] = "/first/does/not/match"
|
|
for index in range(64):
|
|
duplicate["args"].extend(("--project-root", f"/duplicate/does/not/match/{index}"))
|
|
duplicate["args"].extend(("--project-root", str(root)))
|
|
document["mcp"]["servers"][f"{name}-duplicate"] = duplicate
|
|
config = parent / "openclaw.json"
|
|
config.write_text(json.dumps(document), encoding="utf-8")
|
|
ambiguous = run_doctor(project, "openclaw", config_path=config)
|
|
self.assertEqual("unhealthy", ambiguous["doctor_state"])
|
|
self.assertIn(
|
|
"client_entry_ambiguous",
|
|
[check["code"] for check in ambiguous["checks"]],
|
|
)
|
|
|
|
document["mcp"]["servers"].pop(f"{name}-duplicate")
|
|
first["supportsParallelToolCalls"] = "not-a-bool"
|
|
config.write_text(json.dumps(document), encoding="utf-8")
|
|
malformed_parallel = run_doctor(
|
|
project,
|
|
"openclaw",
|
|
config_path=config,
|
|
)
|
|
self.assertEqual("unhealthy", malformed_parallel["doctor_state"])
|
|
|
|
first["supportsParallelToolCalls"] = False
|
|
first["cwd"] = str(parent / "missing-directory")
|
|
config.write_text(json.dumps(document), encoding="utf-8")
|
|
missing_cwd = run_doctor(project, "openclaw", config_path=config)
|
|
self.assertEqual("unhealthy", missing_cwd["doctor_state"])
|
|
self.assertIn(
|
|
"client_cwd_invalid",
|
|
[check["code"] for check in missing_cwd["checks"]],
|
|
)
|
|
|
|
for malformed_filter in (
|
|
["not-an-object"],
|
|
{"include": "not-a-list"},
|
|
{"include": [1, 2]},
|
|
):
|
|
first.pop("cwd", None)
|
|
first["toolFilter"] = malformed_filter
|
|
config.write_text(json.dumps(document), encoding="utf-8")
|
|
malformed = run_doctor(
|
|
project,
|
|
"openclaw",
|
|
config_path=config,
|
|
)
|
|
self.assertEqual("unhealthy", malformed["doctor_state"])
|
|
self.assertIn(
|
|
"client_entry_invalid",
|
|
[check["code"] for check in malformed["checks"]],
|
|
)
|
|
|
|
def test_doctor_bounds_parser_failures_inputs_and_check_inventory(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
project = Project.open(self.copy_fixture(parent))
|
|
deeply_nested = parent / "deep.json"
|
|
deeply_nested.write_text("[" * 2_000 + "]" * 2_000, encoding="utf-8")
|
|
deep = run_doctor(
|
|
project,
|
|
"openclaw",
|
|
config_path=deeply_nested,
|
|
)
|
|
self.assertEqual("unhealthy", deep["doctor_state"])
|
|
|
|
huge_integer = parent / "integer.json"
|
|
huge_integer.write_text(
|
|
'{"mcp":{"servers":{}},"value":' + "9" * 100_000 + "}",
|
|
encoding="utf-8",
|
|
)
|
|
integer = run_doctor(
|
|
project,
|
|
"openclaw",
|
|
config_path=huge_integer,
|
|
)
|
|
self.assertEqual("unhealthy", integer["doctor_state"])
|
|
|
|
malformed_openclaw = parent / "malformed-openclaw.json"
|
|
malformed_openclaw.write_text('{"mcp":"wrong"}', encoding="utf-8")
|
|
malformed_driver = run_doctor(
|
|
project,
|
|
"openclaw",
|
|
config_path=malformed_openclaw,
|
|
)
|
|
self.assertEqual("unhealthy", malformed_driver["doctor_state"])
|
|
self.assertIn(
|
|
"client_config_invalid",
|
|
[check["code"] for check in malformed_driver["checks"]],
|
|
)
|
|
|
|
oversized_name = run_doctor(
|
|
project,
|
|
"openclaw",
|
|
config_path=parent / "missing.json",
|
|
server_name="x" * 5_000,
|
|
)
|
|
Draft202012Validator(DOCTOR_SCHEMA).validate(oversized_name)
|
|
self.assertEqual("unhealthy", oversized_name["doctor_state"])
|
|
self.assertEqual(
|
|
14,
|
|
len(oversized_name["checks"]),
|
|
)
|
|
self.assertEqual(
|
|
len(oversized_name["checks"]),
|
|
len({check["check_id"] for check in oversized_name["checks"]}),
|
|
)
|
|
oversized_path = run_doctor(
|
|
project,
|
|
"openclaw",
|
|
config_path=Path("/" + "x" * 40_000),
|
|
)
|
|
Draft202012Validator(DOCTOR_SCHEMA).validate(oversized_path)
|
|
self.assertEqual("unhealthy", oversized_path["doctor_state"])
|
|
self.assertLessEqual(
|
|
len(json.dumps(oversized_path, sort_keys=True).encode("utf-8")),
|
|
32_768,
|
|
)
|
|
|
|
def test_doctor_rejects_descriptor_config_and_index_parent_swaps(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
descriptor_parent = root / ".docforge"
|
|
moved_descriptor = root / ".docforge-old"
|
|
original_project_open = os.open
|
|
descriptor_swapped = False
|
|
|
|
def swap_descriptor_parent(
|
|
path: object,
|
|
flags: int,
|
|
*args: object,
|
|
**kwargs: object,
|
|
) -> int:
|
|
nonlocal descriptor_swapped
|
|
descriptor = original_project_open(path, flags, *args, **kwargs)
|
|
if (
|
|
not descriptor_swapped
|
|
and Path(path) == descriptor_parent
|
|
and flags & os.O_DIRECTORY
|
|
):
|
|
descriptor_parent.rename(moved_descriptor)
|
|
descriptor_parent.mkdir()
|
|
shutil.copy2(
|
|
moved_descriptor / "project.toml",
|
|
descriptor_parent / "project.toml",
|
|
)
|
|
descriptor_swapped = True
|
|
return descriptor
|
|
|
|
with (
|
|
mock.patch(
|
|
"docforge.project.os.open",
|
|
side_effect=swap_descriptor_parent,
|
|
),
|
|
self.assertRaises(DocForgeError) as changed,
|
|
):
|
|
run_doctor(
|
|
project,
|
|
"codex",
|
|
config_path=parent / "missing.toml",
|
|
)
|
|
self.assertEqual("project_descriptor_changed", changed.exception.code)
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
ProjectIndex(project).build()
|
|
config_parent = parent / "client"
|
|
config_parent.mkdir()
|
|
config = config_parent / "config.toml"
|
|
generated = generate_client_configuration(project, "codex", output=config)
|
|
moved_config = parent / "client-old"
|
|
original_doctor_open = os.open
|
|
config_swapped = False
|
|
|
|
def swap_config_parent(
|
|
path: object,
|
|
flags: int,
|
|
*args: object,
|
|
**kwargs: object,
|
|
) -> int:
|
|
nonlocal config_swapped
|
|
descriptor = original_doctor_open(path, flags, *args, **kwargs)
|
|
if not config_swapped and Path(path) == config_parent and flags & os.O_DIRECTORY:
|
|
config_parent.rename(moved_config)
|
|
config_parent.mkdir()
|
|
(config_parent / "config.toml").write_text("", encoding="utf-8")
|
|
config_swapped = True
|
|
return descriptor
|
|
|
|
with mock.patch(
|
|
"docforge.doctor.os.open",
|
|
side_effect=swap_config_parent,
|
|
):
|
|
changed_config = run_doctor(
|
|
project,
|
|
"codex",
|
|
config_path=config,
|
|
server_name=generated["server_name"],
|
|
)
|
|
self.assertEqual("unhealthy", changed_config["doctor_state"])
|
|
self.assertIn(
|
|
"client_config_changed",
|
|
[check["code"] for check in changed_config["checks"]],
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
ProjectIndex(project).build()
|
|
config = parent / "config.toml"
|
|
generated = generate_client_configuration(project, "codex", output=config)
|
|
index_parent = project.descriptor.index_path.parent
|
|
moved_index_parent = parent / "cache-old"
|
|
original_doctor_open = os.open
|
|
index_swapped = False
|
|
|
|
def swap_index_parent(
|
|
path: object,
|
|
flags: int,
|
|
*args: object,
|
|
**kwargs: object,
|
|
) -> int:
|
|
nonlocal index_swapped
|
|
descriptor = original_doctor_open(path, flags, *args, **kwargs)
|
|
if not index_swapped and Path(path) == index_parent and flags & os.O_DIRECTORY:
|
|
index_parent.rename(moved_index_parent)
|
|
index_parent.mkdir()
|
|
index_swapped = True
|
|
return descriptor
|
|
|
|
with mock.patch(
|
|
"docforge.doctor.os.open",
|
|
side_effect=swap_index_parent,
|
|
):
|
|
unsafe_index = run_doctor(
|
|
project,
|
|
"codex",
|
|
config_path=config,
|
|
server_name=generated["server_name"],
|
|
)
|
|
self.assertEqual("degraded", unsafe_index["doctor_state"])
|
|
self.assertIn(
|
|
"index_unsafe",
|
|
[check["code"] for check in unsafe_index["checks"]],
|
|
)
|
|
|
|
def test_doctor_rechecks_the_exact_client_file_before_return(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
project = Project.open(root)
|
|
ProjectIndex(project).build()
|
|
config = parent / "config.toml"
|
|
generated = generate_client_configuration(project, "codex", output=config)
|
|
raw = config.read_bytes()
|
|
with mock.patch(
|
|
"docforge.doctor._read_stable_regular",
|
|
side_effect=[
|
|
(raw, (1, 1, len(raw), 1, 1)),
|
|
(raw, (1, 2, len(raw), 1, 1)),
|
|
],
|
|
):
|
|
result = run_doctor(
|
|
project,
|
|
"codex",
|
|
config_path=config,
|
|
server_name=generated["server_name"],
|
|
)
|
|
self.assertEqual("unhealthy", result["doctor_state"])
|
|
self.assertIn(
|
|
"client_config_changed",
|
|
[check["code"] for check in result["checks"]],
|
|
)
|
|
|
|
def test_descriptor_currency_and_custom_adapter_generation_fail_closed(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = self.copy_fixture(Path(directory))
|
|
project = Project.open(root)
|
|
descriptor = root / ".docforge" / "project.toml"
|
|
descriptor.write_text(
|
|
descriptor.read_text(encoding="utf-8") + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
with self.assertRaises(DocForgeError) as changed:
|
|
run_doctor(project, "codex", config_path=root / "missing.toml")
|
|
self.assertEqual("source_changed", changed.exception.code)
|
|
|
|
current = Project.open(root)
|
|
current.descriptor = replace(current.descriptor, adapter="custom-adapter")
|
|
with self.assertRaises(DocForgeError) as custom:
|
|
generate_client_configuration(current, "codex")
|
|
self.assertEqual(
|
|
"client_configuration_unavailable",
|
|
custom.exception.code,
|
|
)
|
|
|
|
def test_diagnostic_cli_results_validate_their_dedicated_schemas(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
root = self.copy_fixture(parent)
|
|
preview_output = io.StringIO()
|
|
with contextlib.redirect_stdout(preview_output):
|
|
preview_code = main(
|
|
[
|
|
"--diagnostics",
|
|
"configure",
|
|
"codex",
|
|
"--project",
|
|
str(root),
|
|
]
|
|
)
|
|
self.assertEqual(0, preview_code)
|
|
preview = json.loads(preview_output.getvalue())
|
|
Draft202012Validator(CONFIGURATION_SCHEMA).validate(preview)
|
|
self.assertEqual("cli.configure", preview["diagnostics"]["operation"])
|
|
|
|
unhealthy_output = io.StringIO()
|
|
with contextlib.redirect_stdout(unhealthy_output):
|
|
unhealthy_code = main(
|
|
[
|
|
"--diagnostics",
|
|
"doctor",
|
|
"--client",
|
|
"codex",
|
|
"--project",
|
|
str(root),
|
|
"--config",
|
|
str(parent / "missing.toml"),
|
|
]
|
|
)
|
|
self.assertEqual(2, unhealthy_code)
|
|
unhealthy = json.loads(unhealthy_output.getvalue())
|
|
Draft202012Validator(DOCTOR_SCHEMA).validate(unhealthy)
|
|
self.assertEqual("cli.doctor", unhealthy["diagnostics"]["operation"])
|
|
|
|
def test_configuration_schema_rejects_cross_field_drift(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
project = Project.open(self.copy_fixture(Path(directory)))
|
|
result = generate_client_configuration(project, "codex")
|
|
validator = Draft202012Validator(CONFIGURATION_SCHEMA)
|
|
drifted = json.loads(json.dumps(result))
|
|
drifted["artifact"]["format"] = "openclaw-json-fragment-v1"
|
|
self.assertTrue(list(validator.iter_errors(drifted)))
|
|
drifted = json.loads(json.dumps(result))
|
|
drifted["effective_policy"] = {}
|
|
self.assertTrue(list(validator.iter_errors(drifted)))
|
|
drifted = json.loads(json.dumps(result))
|
|
drifted["action"] = "write"
|
|
self.assertTrue(list(validator.iter_errors(drifted)))
|
|
drifted = json.loads(json.dumps(result))
|
|
drifted["artifact"]["durability"] = "unconfirmed"
|
|
self.assertTrue(list(validator.iter_errors(drifted)))
|
|
drifted = json.loads(json.dumps(result))
|
|
drifted["binding"]["capability_mode"] = "proposal"
|
|
self.assertTrue(list(validator.iter_errors(drifted)))
|
|
drifted = json.loads(json.dumps(result))
|
|
drifted["binding"]["adapter_policy"] = {
|
|
"schema_version": 1,
|
|
"mode": "preserve-no-ast",
|
|
"adapter_evolution": "preserve",
|
|
"ast_forbidden": True,
|
|
"logic_indexing": "off",
|
|
"blocked_tools": ["docforge_get_logic"],
|
|
"instruction": "Preserve adapter behavior without AST or Logic publication.",
|
|
}
|
|
self.assertTrue(list(validator.iter_errors(drifted)))
|
|
drifted = json.loads(json.dumps(result))
|
|
drifted["binding"]["render_policy"]["manual"] = "disabled"
|
|
self.assertTrue(list(validator.iter_errors(drifted)))
|
|
drifted = json.loads(json.dumps(result))
|
|
drifted["binding"]["args"][6] = "proposal"
|
|
self.assertTrue(list(validator.iter_errors(drifted)))
|
|
no_ast = generate_client_configuration(project, "codex", no_ast=True)
|
|
drifted = json.loads(json.dumps(no_ast))
|
|
drifted["binding"]["args"].remove("--no-ast")
|
|
self.assertTrue(list(validator.iter_errors(drifted)))
|
|
drifted = json.loads(json.dumps(result))
|
|
drifted["effective_policy"]["prohibitions"][2] = "made_up_permission"
|
|
self.assertTrue(list(validator.iter_errors(drifted)))
|
|
with self.assertRaises(AssertionError):
|
|
_validate_configuration_result(drifted)
|
|
drifted = json.loads(json.dumps(no_ast))
|
|
drifted["binding"]["adapter_policy"]["instruction"] = "AST use is allowed."
|
|
self.assertTrue(list(validator.iter_errors(drifted)))
|
|
with self.assertRaises(AssertionError):
|
|
_validate_configuration_result(drifted)
|
|
|
|
def test_doctor_schema_rejects_duplicate_inventory_and_inflated_summary(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
parent = Path(directory)
|
|
project = Project.open(self.copy_fixture(parent))
|
|
result = run_doctor(
|
|
project,
|
|
"codex",
|
|
config_path=parent / "missing.toml",
|
|
)
|
|
validator = Draft202012Validator(DOCTOR_SCHEMA)
|
|
duplicate = json.loads(json.dumps(result))
|
|
duplicate["checks"][1]["check_id"] = duplicate["checks"][0]["check_id"]
|
|
self.assertTrue(list(validator.iter_errors(duplicate)))
|
|
inflated = json.loads(json.dumps(result))
|
|
inflated["summary"]["failed"] = 99
|
|
self.assertTrue(list(validator.iter_errors(inflated)))
|
|
|
|
|
|
class GeneratedClientLaunchTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_generated_command_starts_real_project_bound_stdio_server(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory) / "alpha"
|
|
shutil.copytree(FIXTURES / "alpha", root)
|
|
generated = generate_client_configuration(Project.open(root), "codex")
|
|
binding = generated["binding"]
|
|
parameters = StdioServerParameters(
|
|
command=binding["command"],
|
|
args=binding["args"],
|
|
)
|
|
async with (
|
|
stdio_client(parameters) as (read_stream, write_stream),
|
|
ClientSession(read_stream, write_stream) as session,
|
|
):
|
|
await session.initialize()
|
|
tools = await session.list_tools()
|
|
bootstrap = await session.call_tool("docforge_bootstrap", {})
|
|
self.assertEqual(list(READ_TOOLS), [tool.name for tool in tools.tools])
|
|
self.assertEqual("ok", bootstrap.structuredContent["status"])
|
|
self.assertEqual(
|
|
"read", bootstrap.structuredContent["effective_policy"]["capability_mode"]
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|