NODEDC_PLATFORM/infra/deploy-runner/test_engine_node_intelligen...

375 lines
16 KiB
Python

#!/usr/bin/env python3
import hashlib
import importlib.machinery
import importlib.util
import json
import shutil
import tarfile
import tempfile
import unittest
from io import BytesIO
from pathlib import Path
from unittest import mock
SCRIPT_DIR = Path(__file__).resolve().parent
PLATFORM_ROOT = SCRIPT_DIR.parent.parent
ENGINE_ROOT = PLATFORM_ROOT.parent / "NODEDC_ENGINE_INFRA"
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
def load_runner():
loader = importlib.machinery.SourceFileLoader(
"nodedc_engine_node_intelligence_under_test",
str(RUNNER_PATH),
)
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module
RUNNER = load_runner()
def sha256(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
class EngineNodeIntelligenceTest(unittest.TestCase):
def make_image_archive(self, root, runtime_env=None):
runtime_env = list(runtime_env or ["PATH=/usr/local/bin"])
config = {
"architecture": "amd64",
"os": "linux",
"config": {
"Labels": {
"org.opencontainers.image.revision": RUNNER.ENGINE_NODE_INTELLIGENCE_UPSTREAM_COMMIT,
},
"Entrypoint": ["/usr/local/bin/docker-entrypoint.sh"],
"Cmd": ["node", "dist/mcp/index.js"],
"Env": runtime_env,
},
}
config_raw = json.dumps(config, separators=(",", ":")).encode()
config_sha = hashlib.sha256(config_raw).hexdigest()
layer_raw = b"nodedc-node-intelligence-test-layer\n" * 64
layer_sha = hashlib.sha256(layer_raw).hexdigest()
manifest = [{
"Config": f"blobs/sha256/{config_sha}",
"RepoTags": [RUNNER.ENGINE_NODE_INTELLIGENCE_IMAGE],
"Layers": [f"blobs/sha256/{layer_sha}"],
}]
archive = root / "engine-node-intelligence.tar"
with tarfile.open(archive, "w") as output:
for name, value in (
("manifest.json", json.dumps(manifest, separators=(",", ":")).encode()),
(f"blobs/sha256/{config_sha}", config_raw),
(f"blobs/sha256/{layer_sha}", layer_raw),
):
member = tarfile.TarInfo(name)
member.size = len(value)
member.mode = 0o644
output.addfile(member, BytesIO(value))
return archive, config_sha
def make_activation_payload(self, root, runtime_env=None):
payload = root / "payload"
source = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_SOURCE_REL
service = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL
gateway = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
source.mkdir(parents=True)
service.joinpath("image").mkdir(parents=True)
gateway.parent.mkdir(parents=True)
for name in ("catalog.js", "upstreamMcpClient.js", "upstreamProjection.js"):
shutil.copy2(
ENGINE_ROOT / RUNNER.ENGINE_NODE_INTELLIGENCE_SOURCE_REL / name,
source / name,
)
shutil.copy2(
ENGINE_ROOT / RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL,
gateway,
)
readme = service / "README.md"
readme.write_text("# exact node intelligence test\n", encoding="utf-8")
override = service / "docker-compose.immutable-runtime.yml"
override.write_text(
RUNNER.expected_engine_node_intelligence_compose_override(),
encoding="utf-8",
)
archive, config_sha = self.make_image_archive(root, runtime_env)
target_archive = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_IMAGE_ARCHIVE_REL
shutil.copy2(archive, target_archive)
descriptor = {
"schemaVersion": "nodedc.engine-node-intelligence-transition/v1",
"action": "activate",
"releaseId": RUNNER.ENGINE_NODE_INTELLIGENCE_RELEASE_ID,
"expectedCurrent": "inactive",
"upstream": {
"package": "n8n-mcp",
"version": "2.33.2",
"commit": RUNNER.ENGINE_NODE_INTELLIGENCE_UPSTREAM_COMMIT,
},
"image": {
"tag": RUNNER.ENGINE_NODE_INTELLIGENCE_IMAGE,
"archiveRelativePath": RUNNER.ENGINE_NODE_INTELLIGENCE_IMAGE_ARCHIVE_REL,
"archiveSha256": sha256(target_archive),
"configSha256": config_sha,
"architecture": "amd64",
"os": "linux",
},
"source": {
"gatewaySha256": sha256(gateway),
"catalogSha256": sha256(source / "catalog.js"),
"upstreamClientSha256": sha256(source / "upstreamMcpClient.js"),
"upstreamProjectionSha256": sha256(source / "upstreamProjection.js"),
"composeOverrideSha256": sha256(override),
"readmeSha256": sha256(readme),
},
"predecessor": {
"gatewaySha256": RUNNER.ENGINE_NODE_INTELLIGENCE_PREDECESSOR_GATEWAY_SHA256,
"composeSha256": RUNNER.ENGINE_NODE_INTELLIGENCE_PREDECESSOR_COMPOSE_SHA256,
"backendRuntime": "verified-derived-retry",
},
}
(payload / RUNNER.ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL).write_text(
json.dumps(descriptor, indent=2) + "\n",
encoding="utf-8",
)
return payload, descriptor
def test_activation_payload_and_offline_image_are_exact(self):
with tempfile.TemporaryDirectory() as directory:
payload, descriptor = self.make_activation_payload(Path(directory))
actual = RUNNER.validate_engine_node_intelligence_transition(
payload,
RUNNER.ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES,
)
self.assertEqual(actual, descriptor)
def test_gateway_accepts_registered_0_10_and_rejects_unknown_successors(self):
with tempfile.TemporaryDirectory() as directory:
payload, descriptor = self.make_activation_payload(Path(directory))
gateway = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
gateway_text = gateway.read_text(encoding="utf-8")
self.assertIn("const ENGINE_AGENT_MCP_VERSION = '0.10.0'", gateway_text)
gateway.write_text(
gateway_text.replace(
"const ENGINE_AGENT_MCP_VERSION = '0.10.0'",
"const ENGINE_AGENT_MCP_VERSION = '9.9.9'",
),
encoding="utf-8",
)
descriptor["source"]["gatewaySha256"] = sha256(gateway)
(
payload / RUNNER.ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL
).write_text(
json.dumps(descriptor, indent=2) + "\n",
encoding="utf-8",
)
with self.assertRaisesRegex(
RUNNER.DeployError,
"gateway MCP version is not registered",
):
RUNNER.validate_engine_node_intelligence_transition(
payload,
RUNNER.ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES,
)
def test_archive_tamper_and_embedded_authority_fail_closed(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
payload, descriptor = self.make_activation_payload(root)
archive = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_IMAGE_ARCHIVE_REL
archive.write_bytes(archive.read_bytes() + b"tamper")
with self.assertRaisesRegex(RUNNER.DeployError, "archive sha256 mismatch"):
RUNNER.validate_engine_node_intelligence_transition(
payload,
RUNNER.ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES,
)
with tempfile.TemporaryDirectory() as directory:
payload, _descriptor = self.make_activation_payload(
Path(directory),
runtime_env=["AUTH_TOKEN=forbidden"],
)
with self.assertRaisesRegex(RUNNER.DeployError, "contains runtime authority"):
RUNNER.validate_engine_node_intelligence_transition(
payload,
RUNNER.ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES,
)
def test_payload_extra_file_and_wrong_entry_order_are_rejected(self):
with tempfile.TemporaryDirectory() as directory:
payload, _descriptor = self.make_activation_payload(Path(directory))
service = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_SERVICE_ROOT_REL
(service / "unexpected.txt").write_text("no\n", encoding="utf-8")
with self.assertRaisesRegex(RUNNER.DeployError, "service payload file set mismatch"):
RUNNER.validate_engine_node_intelligence_transition(
payload,
RUNNER.ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES,
)
with tempfile.TemporaryDirectory() as directory:
payload, _descriptor = self.make_activation_payload(Path(directory))
with self.assertRaisesRegex(RUNNER.DeployError, "files.txt exact set/order mismatch"):
RUNNER.validate_engine_node_intelligence_transition(
payload,
tuple(reversed(RUNNER.ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES)),
)
def test_predecessor_drift_is_rejected_before_apply(self):
with tempfile.TemporaryDirectory() as payload_dir, tempfile.TemporaryDirectory() as live_dir:
payload, _descriptor = self.make_activation_payload(Path(payload_dir))
live = Path(live_dir)
(live / "nodedc-source/server/routes").mkdir(parents=True)
(live / "docker-compose.yml").write_bytes(
(ENGINE_ROOT / "docker-compose.yml").read_bytes()
)
baseline = bytes.fromhex("00")
gateway = live / RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
gateway.write_bytes(baseline)
original_root = RUNNER.COMPONENTS["engine"]["payload_root"]
RUNNER.COMPONENTS["engine"]["payload_root"] = live
try:
with mock.patch.object(
RUNNER,
"preflight_engine_credential_backend_runtime",
return_value={"mode": "verified-derived-retry"},
):
with self.assertRaisesRegex(RUNNER.DeployError, "gateway drift detected"):
RUNNER.preflight_engine_node_intelligence_predecessor(payload)
finally:
RUNNER.COMPONENTS["engine"]["payload_root"] = original_root
def test_service_selection_and_compose_dispatch_are_narrow(self):
self.assertEqual(
RUNNER.component_services(
"engine",
RUNNER.ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES,
),
(RUNNER.ENGINE_NODE_INTELLIGENCE_SERVICE, "nodedc-backend"),
)
self.assertEqual(
RUNNER.component_services(
"engine",
RUNNER.ENGINE_NODE_INTELLIGENCE_ROLLBACK_ENTRIES,
),
("nodedc-backend",),
)
with (
mock.patch.object(RUNNER, "run_build") as build,
mock.patch.object(RUNNER, "prepare_component_runtime") as prepare,
mock.patch.object(RUNNER, "run_engine_node_intelligence_compose") as exact_compose,
mock.patch.object(RUNNER, "run_compose") as legacy_compose,
):
RUNNER.run_component_runtime(
"engine",
RUNNER.ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES,
(RUNNER.ENGINE_NODE_INTELLIGENCE_SERVICE, "nodedc-backend"),
)
build.assert_called_once()
prepare.assert_called_once()
exact_compose.assert_called_once()
legacy_compose.assert_not_called()
def test_exact_compose_never_recreates_dependencies(self):
services = (RUNNER.ENGINE_NODE_INTELLIGENCE_SERVICE, "nodedc-backend")
with (
mock.patch.object(
RUNNER,
"compose_base_cmd",
return_value=["docker", "compose", "-f", "exact.yml"],
),
mock.patch.object(
RUNNER,
"component_compose_root",
return_value=Path("/exact-engine-root"),
),
mock.patch.object(RUNNER.subprocess, "run") as execute,
):
RUNNER.run_engine_node_intelligence_compose(
services,
RUNNER.ENGINE_NODE_INTELLIGENCE_ARTIFACT_ENTRIES,
)
command = execute.call_args_list[0].args[0]
self.assertIn("--pull", command)
self.assertIn("never", command)
self.assertIn("--no-deps", command)
self.assertEqual(command[-2:], list(services))
def test_backend_additive_mount_projects_into_legacy_guard(self):
descriptor = {"action": "activate"}
baseline_mounts = [
{
"Type": "bind",
"Source": "/baseline/app",
"Destination": "/app",
"RW": True,
},
]
node_mount = {
"Type": "bind",
"Source": str(RUNNER.ENGINE_NODE_INTELLIGENCE_SECRET_FILE),
"Destination": RUNNER.ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH,
"RW": False,
}
container = {"Mounts": [*baseline_mounts, node_mount]}
with (
mock.patch.object(
RUNNER,
"current_engine_node_intelligence_descriptor",
return_value=descriptor,
),
mock.patch.object(
RUNNER,
"validate_installed_engine_node_intelligence_source",
) as source_guard,
mock.patch.object(
RUNNER,
"validate_engine_backend_mounts_for_installed_runtime",
) as legacy_guard,
):
RUNNER.validate_engine_backend_node_intelligence_mounts_for_installed_runtime(
container,
node_modules_read_only=True,
)
source_guard.assert_called_once_with(descriptor)
projected = legacy_guard.call_args.args[0]
self.assertEqual(projected["Mounts"], baseline_mounts)
self.assertTrue(legacy_guard.call_args.args[1])
def test_backend_additive_mount_rejects_source_or_mode_drift(self):
descriptor = {"action": "activate"}
for source, read_write in (("/wrong/source", False), (str(RUNNER.ENGINE_NODE_INTELLIGENCE_SECRET_FILE), True)):
container = {
"Mounts": [{
"Type": "bind",
"Source": source,
"Destination": RUNNER.ENGINE_NODE_INTELLIGENCE_SECRET_CONTAINER_PATH,
"RW": read_write,
}],
}
with (
mock.patch.object(
RUNNER,
"current_engine_node_intelligence_descriptor",
return_value=descriptor,
),
mock.patch.object(
RUNNER,
"validate_installed_engine_node_intelligence_source",
),
):
with self.assertRaisesRegex(
RUNNER.DeployError,
"node-intelligence mount barrier mismatch",
):
RUNNER.validate_engine_backend_node_intelligence_mounts_for_installed_runtime(
container,
node_modules_read_only=True,
)
if __name__ == "__main__":
unittest.main()