Add exact Engine L2 reconciliation transition
This commit is contained in:
@@ -4,10 +4,12 @@ import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
@@ -16,6 +18,11 @@ BUILDER_PATH = SCRIPT_DIR / "build-engine-l2-closed-loop-artifact.mjs"
|
||||
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
|
||||
PATCH_ID = "engine-l2-closed-loop-20991231-999"
|
||||
SOURCE_COMMIT = "dda405cff27977622af0c218abb4d4420c408536"
|
||||
BASELINE_ARTIFACT = (
|
||||
SCRIPT_DIR.parent
|
||||
/ "deploy-artifacts"
|
||||
/ "nodedc-engine-provider-authority-diagnostics-20260723-029.tgz"
|
||||
)
|
||||
|
||||
|
||||
def load_runner():
|
||||
@@ -57,6 +64,37 @@ class EngineL2ClosedLoopArtifactTest(unittest.TestCase):
|
||||
)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
def load_built_payload(self, root):
|
||||
built = self.build(root / "artifact")
|
||||
artifact = Path(built["artifact"])
|
||||
manifest, entries, payload = RUNNER.load_artifact(
|
||||
artifact,
|
||||
root / "loaded",
|
||||
)
|
||||
return built, manifest, entries, payload
|
||||
|
||||
def materialize_partial_predecessor(self, payload, entries, root):
|
||||
for relative_path in entries:
|
||||
source = payload / relative_path
|
||||
destination = root / relative_path
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if source.is_dir():
|
||||
shutil.copytree(source, destination)
|
||||
else:
|
||||
shutil.copy2(source, destination)
|
||||
with tarfile.open(BASELINE_ARTIFACT, "r:gz") as archive:
|
||||
member = archive.extractfile(
|
||||
"payload/nodedc-source/services/node-intelligence/activation.json"
|
||||
)
|
||||
self.assertIsNotNone(member)
|
||||
(root / RUNNER.ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL).write_bytes(
|
||||
member.read()
|
||||
)
|
||||
shutil.copytree(
|
||||
root / "nodedc-source/dist",
|
||||
root / "nginx-html",
|
||||
)
|
||||
|
||||
def test_builder_is_commit_bound_and_byte_deterministic(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-l2-closed-loop-") as directory:
|
||||
root = Path(directory)
|
||||
@@ -74,6 +112,30 @@ class EngineL2ClosedLoopArtifactTest(unittest.TestCase):
|
||||
self.assertEqual(first["services"], ["nodedc-backend", "app"])
|
||||
self.assertEqual(first["mcpVersion"], "0.7.0")
|
||||
|
||||
def test_failed_030_identity_is_terminal_and_cannot_be_retried(self):
|
||||
with self.assertRaisesRegex(
|
||||
RUNNER.DeployError,
|
||||
"terminal failed",
|
||||
):
|
||||
RUNNER.reject_terminal_engine_l2_failed_artifact(
|
||||
{
|
||||
"id": RUNNER.ENGINE_L2_CLOSED_LOOP_FAILED_PATCH_ID,
|
||||
},
|
||||
"0" * 64,
|
||||
)
|
||||
with self.assertRaisesRegex(
|
||||
RUNNER.DeployError,
|
||||
"terminal failed",
|
||||
):
|
||||
RUNNER.reject_terminal_engine_l2_failed_artifact(
|
||||
{"id": "different-successor"},
|
||||
RUNNER.ENGINE_L2_CLOSED_LOOP_FAILED_ARTIFACT_SHA256,
|
||||
)
|
||||
RUNNER.reject_terminal_engine_l2_failed_artifact(
|
||||
{"id": "engine-l2-closed-loop-20260723-031"},
|
||||
"1" * 64,
|
||||
)
|
||||
|
||||
def test_artifact_is_a_safe_exact_engine_overlay(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-l2-overlay-") as directory:
|
||||
root = Path(directory)
|
||||
@@ -134,6 +196,192 @@ class EngineL2ClosedLoopArtifactTest(unittest.TestCase):
|
||||
self.assertTrue(all("/logs/" not in path for path in payload_files))
|
||||
self.assertTrue(all(not path.endswith(".env") for path in payload_files))
|
||||
|
||||
def test_preflight_accepts_only_the_exact_failed_030_partial_state(self):
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-l2-preflight-") as directory:
|
||||
workspace = Path(directory)
|
||||
_built, _manifest, entries, payload = self.load_built_payload(
|
||||
workspace / "build"
|
||||
)
|
||||
live = workspace / "live"
|
||||
self.materialize_partial_predecessor(payload, entries, live)
|
||||
recovery = workspace / RUNNER.ENGINE_L2_CLOSED_LOOP_RECOVERY_BACKUP_ID
|
||||
|
||||
backend = {
|
||||
"mode": "verified-derived-retry",
|
||||
"container_id": "a" * 64,
|
||||
}
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=live),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_engine_l2_closed_loop_recovery_evidence",
|
||||
return_value=recovery,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_installed_engine_node_intelligence_source",
|
||||
) as validate_source,
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"preflight_engine_credential_backend_runtime",
|
||||
return_value=backend,
|
||||
) as validate_backend,
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"engine_compose_service_container_id_for_gateway",
|
||||
return_value="b" * 64,
|
||||
) as app_lookup,
|
||||
):
|
||||
result = RUNNER.preflight_engine_l2_closed_loop_predecessor(
|
||||
payload
|
||||
)
|
||||
self.assertEqual(
|
||||
result["mode"],
|
||||
"failed-030-partial-source-reconciliation",
|
||||
)
|
||||
self.assertEqual(result["backend_container_id"], "a" * 64)
|
||||
self.assertEqual(result["app_container_id"], "b" * 64)
|
||||
self.assertEqual(
|
||||
result["target_gateway_sha256"],
|
||||
RUNNER.ENGINE_L2_CLOSED_LOOP_TARGET_SHA256[
|
||||
RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
validate_source.call_args.kwargs[
|
||||
"expected_gateway_sha256"
|
||||
],
|
||||
RUNNER.ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
|
||||
RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
validate_backend.call_args.kwargs[
|
||||
"expected_node_intelligence_gateway_sha256"
|
||||
],
|
||||
RUNNER.ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
|
||||
RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
||||
],
|
||||
)
|
||||
app_lookup.assert_called_once_with(
|
||||
"app",
|
||||
RUNNER.ENGINE_L2_CLOSED_LOOP_PARTIAL_SHA256[
|
||||
RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
||||
],
|
||||
)
|
||||
|
||||
index_path = live / "nodedc-source/dist/index.html"
|
||||
index_path.write_bytes(index_path.read_bytes() + b"\n")
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=live),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_engine_l2_closed_loop_recovery_evidence",
|
||||
return_value=recovery,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"preflight_engine_credential_backend_runtime",
|
||||
) as unreachable_backend,
|
||||
):
|
||||
with self.assertRaisesRegex(
|
||||
RUNNER.DeployError,
|
||||
"partial predecessor drift",
|
||||
):
|
||||
RUNNER.preflight_engine_l2_closed_loop_predecessor(payload)
|
||||
unreachable_backend.assert_not_called()
|
||||
|
||||
def test_reconciliation_rollback_uses_failed_030_backup_and_restores_runtime(self):
|
||||
entries = RUNNER.ENGINE_L2_CLOSED_LOOP_ARTIFACT_ENTRIES
|
||||
services = ("nodedc-backend", "app")
|
||||
with tempfile.TemporaryDirectory(prefix="nodedc-engine-l2-rollback-") as directory:
|
||||
root = Path(directory)
|
||||
candidate = root / "candidate-backup"
|
||||
recovery = root / "recovery-backup"
|
||||
candidate_entries = [*entries, "nginx-html"]
|
||||
original_entries = [
|
||||
rel
|
||||
for rel in entries
|
||||
if rel != RUNNER.ENGINE_L2_CLOSED_LOOP_DESCRIPTOR_REL
|
||||
]
|
||||
original_entries.append("nginx-html")
|
||||
|
||||
def backup_paths(path):
|
||||
if path.parent == candidate:
|
||||
return candidate_entries if path.name == "existing-files.txt" else []
|
||||
if path.parent == recovery:
|
||||
if path.name == "existing-files.txt":
|
||||
return [
|
||||
rel
|
||||
for rel in original_entries
|
||||
if rel != "nodedc-source/server/l2/graphRepository.js"
|
||||
]
|
||||
return ["nodedc-source/server/l2/graphRepository.js"]
|
||||
raise AssertionError(path)
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_engine_l2_closed_loop_recovery_evidence",
|
||||
return_value=recovery,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"read_backup_path_list",
|
||||
side_effect=backup_paths,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"restore_platform_overlay",
|
||||
side_effect=(len(candidate_entries), len(original_entries)),
|
||||
) as restore,
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_engine_l2_closed_loop_stable_source",
|
||||
) as validate_stable,
|
||||
mock.patch.object(RUNNER, "run_component_runtime") as run_runtime,
|
||||
mock.patch.object(RUNNER, "healthcheck_compose_service") as check_service,
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"component_healthchecks",
|
||||
return_value=(
|
||||
"http://127.0.0.1:8080/",
|
||||
"http://127.0.0.1:3001/health",
|
||||
),
|
||||
),
|
||||
mock.patch.object(RUNNER, "healthcheck_url") as check_url,
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"preflight_engine_credential_backend_runtime",
|
||||
return_value={"mode": "verified-derived-retry"},
|
||||
),
|
||||
):
|
||||
result = RUNNER.rollback_engine_l2_closed_loop_reconciliation(
|
||||
root,
|
||||
candidate,
|
||||
entries,
|
||||
"20991231-235959",
|
||||
True,
|
||||
services,
|
||||
)
|
||||
|
||||
self.assertTrue(result.startswith("stable-source+runtime-restored:"))
|
||||
self.assertEqual(
|
||||
[call.args[1] for call in restore.call_args_list],
|
||||
[candidate, recovery],
|
||||
)
|
||||
validate_stable.assert_called_once_with(root)
|
||||
run_runtime.assert_called_once_with(
|
||||
"engine",
|
||||
tuple(RUNNER.ENGINE_L2_CLOSED_LOOP_STABLE_SHA256),
|
||||
services,
|
||||
)
|
||||
self.assertEqual(
|
||||
[call.args for call in check_service.call_args_list],
|
||||
[("engine", "nodedc-backend"), ("engine", "app")],
|
||||
)
|
||||
self.assertEqual(check_url.call_count, 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
Reference in New Issue
Block a user