diff --git a/PUBLIC_SOURCE_EXPORT.md b/PUBLIC_SOURCE_EXPORT.md index 8c4b1fa..73d96e6 100644 --- a/PUBLIC_SOURCE_EXPORT.md +++ b/PUBLIC_SOURCE_EXPORT.md @@ -1,3 +1,3 @@ # Public source export -This source snapshot was generated from the private canonical repository at revision `8823cc4d95756a93815c82b8ab62215be3d63862`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. +This source snapshot was generated from the private canonical repository at revision `d53d0423bb4ff61be689fc6088ee05ecbb69d8f6`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index 1f4be85..a93d8f6 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "sourceRevision": "8823cc4d95756a93815c82b8ab62215be3d63862", + "sourceRevision": "d53d0423bb4ff61be689fc6088ee05ecbb69d8f6", "files": [ { "path": ".dockerignore", @@ -3162,6 +3162,11 @@ "sha256": "defe158fc2e1641a220e52f40d658230b445d505680e0799d722aef407b1b0ae", "bytes": 2497 }, + { + "path": "tests/test_contextforge_safe_diagnostic.py", + "sha256": "fc32191da5e78a1ae5a906c8168a33cbeda719ba0120fbd68caf47c5ea852484", + "bytes": 4276 + }, { "path": "tests/test_contextforge_session_affinity_contract.py", "sha256": "84cc24fec155594bb5fc97a6966f2312b81d9b80dce22a2903c17de45797f006", diff --git a/tests/test_contextforge_safe_diagnostic.py b/tests/test_contextforge_safe_diagnostic.py new file mode 100644 index 0000000..e6dac2d --- /dev/null +++ b/tests/test_contextforge_safe_diagnostic.py @@ -0,0 +1,115 @@ +import subprocess +from types import SimpleNamespace + +import pytest + +from scripts import build_mcp_hub_release_images as producer + + +def test_diagnostic_reports_only_closed_values_and_handles_split_tokens(): + diagnostic = producer.ContextForgeDiagnostic() + diagnostic.feed( + b"=== BUILD UPSTREAM SOURCE IMAGE ===\nsecret=never-export\nCould not resolve ho" + ) + diagnostic.feed(b"st: private.example\n") + assert diagnostic.stage == "upstream_build" + assert diagnostic.category == "dns" + assert "secret" not in diagnostic.message(1) + assert "private" not in diagnostic.message(1) + + +def test_new_stage_resets_previous_cause_and_memory_is_bounded(): + diagnostic = producer.ContextForgeDiagnostic() + diagnostic.feed(b"=== FETCH EXACT CONTEXTFORGE SOURCE ===\ncertificate verify failed\n") + assert diagnostic.category == "tls" + diagnostic.feed(b"=== VERIFY RUNTIME CONTRACT ===\n" + b"x" * 1_000_000) + assert diagnostic.stage == "runtime_verify" + assert diagnostic.category == "unknown" + assert len(diagnostic.window) <= 4096 + + +def test_unrecognized_and_secret_shaped_error_is_never_echoed(): + diagnostic = producer.ContextForgeDiagnostic() + diagnostic.feed(b"Authorization: Bearer fixture-sensitive-value\n") + assert diagnostic.message(1) == ( + "fixed release stage failed: stage=contextforge_build category=nonzero_exit " + "exitCode=1 substep=validation cause=unknown" + ) + + +@pytest.mark.parametrize("failure", ["exit", "timeout", "register", "read", "success"]) +def test_fixed_process_discards_output_and_always_cleans_up(monkeypatch, tmp_path, failure): + closed = [] + terminated = [] + + class Stream: + def fileno(self): + return 12 + + def close(self): + closed.append("stream") + + code = 0 if failure == "success" else 1 + process = SimpleNamespace(stdout=Stream(), poll=lambda: code, wait=lambda timeout: code) + + def popen(command, **kwargs): + assert command == [ + "/bin/bash", + "scripts/build_contextforge_compat_image.sh", + "--release-candidate", + ] + assert kwargs["stdin"] == subprocess.DEVNULL + assert kwargs["stderr"] == subprocess.STDOUT + assert kwargs["start_new_session"] is True + return process + + class Selector: + def __init__(self): + self.active = True + + def register(self, *_args): + if failure == "register": + raise OSError("fixture-sensitive-registration") + + def get_map(self): + return {12: True} if self.active else {} + + def select(self, **_kwargs): + return [(SimpleNamespace(fd=12, fileobj=process.stdout), 1)] + + def unregister(self, _stream): + self.active = False + + def close(self): + closed.append("selector") + + blocks = iter([b"=== FETCH EXACT CONTEXTFORGE SOURCE ===\nPermission denied secret\n", b""]) + ticks = iter([0, producer.COMMAND_TIMEOUT + 1 if failure == "timeout" else 1, 2]) + monkeypatch.setattr(producer.subprocess, "Popen", popen) + monkeypatch.setattr(producer.selectors, "DefaultSelector", Selector) + monkeypatch.setattr(producer.os, "set_blocking", lambda *_args: None) + + def read(*_args): + if failure == "read": + raise OSError("fixture-sensitive-read") + return next(blocks) + + monkeypatch.setattr(producer.os, "read", read) + monkeypatch.setattr(producer.time, "monotonic", lambda: next(ticks)) + monkeypatch.setattr(producer, "_terminate_child", lambda child: terminated.append(child)) + if failure == "success": + producer._run_contextforge_diagnostic(tmp_path, {}) + else: + with pytest.raises(producer.BuildRejected) as error: + producer._run_contextforge_diagnostic(tmp_path, {}) + assert "secret" not in str(error.value) + assert "fixture-sensitive" not in str(error.value) + expected = { + "exit": "substep=source_fetch cause=permission", + "timeout": "category=timeout", + "register": "category=io_error", + "read": "category=io_error", + }[failure] + assert expected in str(error.value) + assert closed == ["selector", "stream"] + assert terminated == [process]