"""Hermetic contract tests for the Claude Code IOP benchmark adapter.""" from __future__ import annotations import json import os import tempfile import textwrap import unittest from pathlib import Path from unittest.mock import patch from scripts.agent_benchmark.claude_iop import ( ClaudeIopAdapter, ClaudeIopProtocolError, ClaudeIopRuntime, ClaudeIopValidationError, ClaudeStreamParser, parse_preflight_binding, redact_claude_event, ) from scripts.agent_benchmark.lifecycle import REASON_PARSER_ERROR, REASON_SUCCESS, run_invocation from scripts.agent_benchmark.manifest import ExpectedBinding, IopCell, MatrixCell, Timeout from scripts.agent_benchmark.workspace import ( AttemptIdentity, PreparedWorkspace, TestbedProvenance, ) SENTINELS = ("prompt-secret-sentinel", "https://private.iop.invalid", "api-secret-sentinel") ARBITRARY_SENTINELS = ( "tool-secret-sentinel", "result-secret-sentinel", "error-secret-sentinel", ) def _cell(route_kind: str = "direct") -> MatrixCell: bindings = (ExpectedBinding("request", "claude-sonnet", "high"),) if route_kind == "execution_preset": bindings = ( ExpectedBinding("selector", "claude-sonnet", "high"), ExpectedBinding("plan", "claude-sonnet", "high"), ExpectedBinding("work", "claude-sonnet", "high"), ExpectedBinding("review", "claude-sonnet", "high"), ) return MatrixCell("claude-direct", "claude", IopCell( "claude-sonnet", "high", route_kind, "iop-route", bindings, )) def _workspace(root: Path, session_id: str = "session-fixture") -> PreparedWorkspace: workspace = root / "workspace" workspace.mkdir() return PreparedWorkspace( AttemptIdentity("run-20260102T030405Z-abcdef123456", "claude-direct", 1, 1), str(root), str(workspace), str(root / "session"), session_id, True, "sha256:" + "0" * 64, "isolated", TestbedProvenance("/testbed", "main", "0" * 40, "sha256:" + "1" * 64, True), "2026-01-02T03:04:05Z", ) def _binding_event(cell: MatrixCell) -> str: return json.dumps({ "type": "system", "subtype": "iop_binding", "binding": { "cell_id": cell.id, "caller": cell.caller, "requested_route_kind": cell.iop.route_kind, "requested_route_id": cell.iop.route_id, "requested_model": cell.iop.request_model, "requested_effort": cell.iop.requested_effort, "effective_route_kind": cell.iop.route_kind, "effective_route_id": cell.iop.route_id, "effective_model": cell.iop.request_model, "effective_effort": cell.iop.requested_effort, "effective_bindings": [ {"stage": item.stage, "model": item.model, "effort": item.effort} for item in cell.iop.expected_bindings ], }, }) class ClaudeIopTest(unittest.TestCase): def setUp(self) -> None: # The lifecycle intentionally requires an executable fake CLI. Some # CI hosts mount /tmp noexec, so keep this short-lived directory under # the repository worktree instead. self.temp = tempfile.TemporaryDirectory(dir=Path.cwd(), prefix=".claude-iop-test-") self.root = Path(self.temp.name) self.cell = _cell() self.workspace = _workspace(self.root) self.binary = self.root / "claude" self.binary.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") self.binary.chmod(0o700) self.runtime = ClaudeIopRuntime(str(self.binary), SENTINELS[1], SENTINELS[2]) def tearDown(self) -> None: self.temp.cleanup() def _adapter(self, *, cell: MatrixCell | None = None) -> ClaudeIopAdapter: return ClaudeIopAdapter(cell or self.cell, self.workspace, self.runtime) def _fixture_lines(self) -> list[str]: fixture = Path("scripts/fixtures/agent-comparison-benchmark/claude-iop-stream.jsonl") return fixture.read_text(encoding="utf-8").splitlines() def _run_fake(self, lines: list[str]): self.binary.write_text(textwrap.dedent(f"""\ #!/usr/bin/env python3 import os, sys assert sys.stdin.read() == {SENTINELS[0]!r} assert os.environ["ANTHROPIC_BASE_URL"] == {SENTINELS[1]!r} assert os.environ["ANTHROPIC_API_KEY"] == {SENTINELS[2]!r} for line in {lines!r}: print(line) """), encoding="utf-8") evidence = self.root / f"evidence-{len(tuple(self.root.glob('evidence-*')))}" evidence.mkdir() adapter = self._adapter() result = run_invocation( adapter.invocation(SENTINELS[0], evidence, Timeout(5, 1, 1, 1)), parse_event=adapter.parser(), redact=adapter.redactor(SENTINELS[0]), on_started=lambda _: None, ) durable = "\n".join( path.read_text(encoding="utf-8") for path in sorted(evidence.iterdir()) ) return result, durable def test_exact_iop_only_invocation_and_fresh_workspace(self) -> None: adapter = self._adapter() with patch.dict(os.environ, {"SSL_CERT_FILE": "/operator/dev-ca.pem", "NODE_EXTRA_CA_CERTS": "/operator/dev-ca.pem"}): spec = adapter.invocation(SENTINELS[0], self.root / "evidence", Timeout(5, 1, 1, 1)) self.assertEqual(spec.cwd, self.workspace.workspace_dir) self.assertEqual(spec.submission_mode, "stdin_once") self.assertEqual(spec.task_payload, SENTINELS[0].encode()) self.assertEqual(spec.argv[1:], ( "--bare", "--print", "--verbose", "--input-format", "text", "--output-format", "stream-json", "--model", "claude-sonnet", "--effort", "high", "--no-session-persistence", "--permission-mode", "dontAsk", "--tools", "Read,Write,Edit", "--allowedTools", "Read,Write,Edit", )) env = dict(spec.env) self.assertEqual(env["ANTHROPIC_BASE_URL"], SENTINELS[1]) self.assertEqual(env["ANTHROPIC_API_KEY"], SENTINELS[2]) self.assertEqual(env["SSL_CERT_FILE"], "/operator/dev-ca.pem") self.assertEqual(env["NODE_EXTRA_CA_CERTS"], "/operator/dev-ca.pem") self.assertNotIn("ANTHROPIC_AUTH_TOKEN", env) self.assertNotIn("CLAUDE_CONFIG_DIR", env) (self.root / "other").mkdir() not_fresh = _workspace(self.root / "other", "session-other") object.__setattr__(not_fresh, "session_is_fresh", False) with self.assertRaises(ClaudeIopValidationError): ClaudeIopAdapter(self.cell, not_fresh, self.runtime) def test_direct_and_preset_preflight_are_exact_without_substitution(self) -> None: for route_kind in ("direct", "execution_preset"): cell = _cell(route_kind) event = _binding_event(cell) adapter = self._adapter(cell=cell) self.assertEqual(adapter.preflight(event).status, "ready") mutated = json.loads(event) mutated["binding"]["effective_model"] = "fallback" with self.assertRaises(ClaudeIopProtocolError): parse_preflight_binding(json.dumps(mutated), cell) incomplete = json.loads(_binding_event(self.cell)) del incomplete["binding"]["effective_effort"] with self.assertRaises(ClaudeIopProtocolError): self._adapter().preflight(json.dumps(incomplete)) def test_runtime_requires_available_binary_and_complete_iop_config(self) -> None: missing_binary = ClaudeIopRuntime( str(self.root / "missing-claude"), SENTINELS[1], SENTINELS[2] ) with self.assertRaises(ClaudeIopValidationError): ClaudeIopAdapter(self.cell, self.workspace, missing_binary) for base_url, api_key in (("", SENTINELS[2]), (SENTINELS[1], "")): with self.assertRaises(ClaudeIopValidationError): ClaudeIopAdapter( self.cell, self.workspace, ClaudeIopRuntime(str(self.binary), base_url, api_key), ) def test_fixture_uses_production_shaped_ordered_terminal_evidence(self) -> None: lines = self._fixture_lines() parser = ClaudeStreamParser(self.cell, "session-fixture") parsed = [parser("stdout", line) for line in lines] self.assertEqual(parsed[:2], [None, "finish"]) self.assertEqual(parsed[2][-1], "idle") malformed = json.loads(lines[1]) del malformed["message"]["model"] with self.assertRaises(ClaudeIopProtocolError): parser = ClaudeStreamParser(self.cell, "session-fixture") parser("stdout", lines[0]) parser("stdout", json.dumps(malformed)) with self.assertRaises(ClaudeIopProtocolError): parser("stdout", "not-json") def test_parser_rejects_missing_duplicate_mismatched_and_out_of_order_evidence(self) -> None: init, assistant, result = self._fixture_lines() wrong_session = json.loads(assistant) wrong_session["session_id"] = "other-claude-session" wrong_model = json.loads(assistant) wrong_model["message"]["model"] = "fallback-model" missing_session = json.loads(result) del missing_session["session_id"] missing_result = [init, assistant] cases = { "assistant-before-init": [assistant], "result-before-assistant": [init, result], "duplicate-init": [init, init], "duplicate-assistant": [init, assistant, assistant], "session-mismatch": [init, json.dumps(wrong_session)], "nested-model-mismatch": [init, json.dumps(wrong_model)], "missing-result-session": [init, assistant, json.dumps(missing_session)], } for name, events in cases.items(): with self.subTest(name=name): parser = ClaudeStreamParser(self.cell, "session-fixture") with self.assertRaises(ClaudeIopProtocolError): for event in events: parser("stdout", event) parser = ClaudeStreamParser(self.cell, "session-fixture") self.assertEqual([parser("stdout", event) for event in missing_result], [None, "finish"]) def test_parser_accepts_partial_snapshots_and_tool_result_cycles(self) -> None: init, _, result = self._fixture_lines() parser = ClaudeStreamParser(self.cell, "session-fixture") partial = json.dumps({ "type": "assistant", "session_id": "claude-session-fixture", "message": {"id": "msg-tool-1", "model": "claude-sonnet", "stop_reason": None, "content": [{"type": "tool_use", "id": "tool-1"}]}, }) user = json.dumps({ "type": "user", "session_id": "claude-session-fixture", "message": {"content": [{"type": "tool_result", "tool_use_id": "tool-1"}]}, }) final = json.dumps({ "type": "assistant", "session_id": "claude-session-fixture", "message": {"id": "msg-final", "model": "claude-sonnet", "stop_reason": "end_turn", "content": []}, }) self.assertEqual( [parser("stdout", line) for line in (init, partial, partial, user, final)], [None, None, None, None, "finish"], ) observations = parser("stdout", result) metrics = {metric.name: metric.value for metric in observations[:-1]} self.assertEqual(metrics["model_calls"], 2) def test_parser_accepts_cumulative_message_ids_and_direct_result(self) -> None: init, _, result = self._fixture_lines() parser = ClaudeStreamParser(self.cell, "session-fixture") partial = json.dumps({ "type": "assistant", "session_id": "claude-session-fixture", "message": {"id": "msg-cumulative", "model": "claude-sonnet", "stop_reason": None, "content": []}, }) user = json.dumps({ "type": "user", "session_id": "claude-session-fixture", "message": {"content": []}, }) terminal = json.loads(result) terminal["usage"].update({ "cache_creation": {"ephemeral_1h_input_tokens": 0, "ephemeral_5m_input_tokens": 0}, "inference_geo": "", "iterations": [], "output_tokens_details": {"thinking_tokens": 0}, "server_tool_use": {"web_fetch_requests": 0, "web_search_requests": 0}, "service_tier": "standard", "speed": "standard", }) parsed = [ parser("stdout", line) for line in (init, partial, partial, user, partial, user, user, partial) ] self.assertEqual(parsed, [None] * len(parsed)) observations = parser("stdout", json.dumps(terminal)) self.assertEqual(observations[-1], "idle") self.assertEqual( {metric.name: metric.value for metric in observations[:-1]}["model_calls"], 1 ) def test_parser_classifies_synthetic_api_error_without_parser_failure(self) -> None: init, _, _ = self._fixture_lines() parser = ClaudeStreamParser(self.cell, "session-fixture") synthetic = json.dumps({ "type": "assistant", "session_id": "claude-session-fixture", "is_api_error_message": True, "message": {"id": "synthetic", "model": "", "stop_reason": "stop_sequence", "content": []}, }) terminal = json.dumps({ "type": "result", "session_id": "claude-session-fixture", "subtype": "success", "is_error": True, "terminal_reason": "api_error", }) self.assertEqual( [parser("stdout", line) for line in (init, synthetic, terminal)], [None, None, ()], ) def test_parser_accepts_explicit_tool_use_terminal_once(self) -> None: init, _, result = self._fixture_lines() parser = ClaudeStreamParser(self.cell, "session-fixture") tool_use = json.dumps({ "type": "assistant", "session_id": "claude-session-fixture", "message": {"id": "msg-tool-1", "model": "claude-sonnet", "stop_reason": "tool_use", "content": []}, }) user = json.dumps({ "type": "user", "session_id": "claude-session-fixture", "message": {"content": []}, }) final = json.dumps({ "type": "assistant", "session_id": "claude-session-fixture", "message": {"id": "msg-final", "model": "claude-sonnet", "stop_reason": "end_turn", "content": []}, }) self.assertEqual( [parser("stdout", line) for line in (init, tool_use, user, final)], [None, None, None, "finish"], ) self.assertEqual( {metric.name: metric.value for metric in parser("stdout", result)[:-1]}["model_calls"], 2, ) duplicate = ClaudeStreamParser(self.cell, "session-fixture") duplicate("stdout", init) duplicate("stdout", tool_use) duplicate("stdout", user) with self.assertRaises(ClaudeIopProtocolError): duplicate("stdout", tool_use) def test_reported_result_values_become_bound_normalized_observations(self) -> None: init, assistant, result = self._fixture_lines() parser = ClaudeStreamParser(self.cell, "session-fixture") parser("stdout", init) parser("stdout", assistant) parsed = parser("stdout", result) self.assertEqual(parsed[-1], "idle") observed = {metric.name: metric for metric in parsed[:-1]} self.assertEqual(observed["total_duration"].value, 1234 * 10 ** 6) self.assertFalse(observed["total_duration"].overlap) # The reported API duration is inside the reported total, so it is # published as an overlapping interval and never subtracted from it. self.assertEqual(observed["model_duration"].value, 1000 * 10 ** 6) self.assertTrue(observed["model_duration"].overlap) self.assertEqual(observed["model_calls"].value, 1) self.assertEqual(observed["input_tokens"].value, 11) self.assertEqual(observed["output_tokens"].value, 22) self.assertEqual(observed["cached_input_tokens"].value, 5) # The fixture omits cache creation, so that category stays unreported # rather than being reported as zero. self.assertNotIn("cache_write_tokens", observed) for metric in parsed[:-1]: self.assertEqual(metric.model, "claude-sonnet") self.assertEqual(metric.source, "caller_output") self.assertEqual(observed["input_tokens"].clock, "none") self.assertEqual(observed["total_duration"].clock, "caller_reported") def test_unknown_negative_boolean_and_fractional_usage_fails_closed(self) -> None: init, assistant, result = self._fixture_lines() base = json.loads(result) cases = { "unknown-usage-key": {"usage": {"input_tokens": 1, "web_search_requests": 2}}, "fractional-token": {"usage": {"input_tokens": 1.5}}, "boolean-token": {"usage": {"input_tokens": True}}, "negative-token": {"usage": {"input_tokens": -1}}, "string-duration": {"duration_ms": "1234"}, "boolean-duration": {"duration_ms": True}, "negative-duration": {"duration_ms": -5}, "non-object-usage": {"usage": [1, 2]}, } for name, override in cases.items(): with self.subTest(name=name): parser = ClaudeStreamParser(self.cell, "session-fixture") parser("stdout", init) parser("stdout", assistant) with self.assertRaises(ClaudeIopProtocolError): parser("stdout", json.dumps({**base, **override})) def test_structural_redaction_never_retains_sensitive_content(self) -> None: raw = json.dumps({ "type": "result", "result": "result-secret-sentinel", "message": "error-secret-sentinel", "content": "prompt-secret-sentinel", "tool_input": {"arguments": "tool-secret-sentinel"}, "diagnostic": "https://private.iop.invalid api-secret-sentinel", }) redacted = redact_claude_event(raw, SENTINELS) error_redacted = redact_claude_event(json.dumps({ "type": "error", "message": "error-secret-sentinel", "error": {"detail": "tool-secret-sentinel"}, }), SENTINELS) for sentinel in (*SENTINELS, *ARBITRARY_SENTINELS): self.assertNotIn(sentinel, redacted) self.assertNotIn(sentinel, error_redacted) self.assertIn("[redacted]", redacted) self.assertEqual(redact_claude_event("raw prompt-secret-sentinel", SENTINELS), '{"type":"invalid_claude_json"}') def test_fake_cli_runs_once_and_durable_evidence_is_redacted(self) -> None: init, assistant, result = (json.loads(line) for line in self._fixture_lines()) assistant["message"]["content"] = SENTINELS[0] assistant["tool_input"] = {"arguments": ARBITRARY_SENTINELS[0]} result["result"] = ARBITRARY_SENTINELS[1] diagnostic = {"type": "system", "subtype": "notice", "error": ARBITRARY_SENTINELS[2]} outcome, durable = self._run_fake([ json.dumps(init), json.dumps(diagnostic), json.dumps(assistant), json.dumps(result), ]) result = outcome self.assertTrue(result.success, result) self.assertEqual(result.terminal_reason, REASON_SUCCESS) self.assertTrue(result.submitted) self.assertTrue(result.finish_then_idle_then_quiet) for sentinel in (*SENTINELS, *ARBITRARY_SENTINELS): self.assertNotIn(sentinel, durable) self.assertEqual( [metric.name for metric in result.metrics], [ "total_duration", "model_duration", "model_calls", "cached_input_tokens", "input_tokens", "output_tokens", ], ) # Numeric observations survive the content redactors that removed every # sentinel above, and the durable journal keeps them in one closed shape. self.assertIn('"kind": "metric:total_duration"', durable) self.assertIn('\\"value\\":1234000000', durable) def test_lifecycle_rejects_boundary_violations(self) -> None: init, assistant, result = self._fixture_lines() bad_model = json.loads(assistant) bad_model["message"]["model"] = "fallback-model" cases = { "missing-init": [assistant, result], "missing-result": [init, assistant], "duplicate-init": [init, init, assistant, result], "out-of-order-result": [init, result, assistant], "mismatched-model": [init, json.dumps(bad_model), result], "malformed-json": [init, "not-json", assistant, result], } for name, lines in cases.items(): with self.subTest(name=name): outcome, durable = self._run_fake(lines) self.assertFalse(outcome.success, outcome) for sentinel in (*SENTINELS, *ARBITRARY_SENTINELS): self.assertNotIn(sentinel, durable) def test_metric_prefixed_malformed_output_is_redacted_before_durable_capture(self) -> None: metric_sentinel = "metric:" + SENTINELS[0] outcome, durable = self._run_fake([metric_sentinel]) self.assertFalse(outcome.success, outcome) self.assertEqual(outcome.terminal_reason, REASON_PARSER_ERROR) self.assertTrue(outcome.submitted) self.assertIn("invalid_claude_json", durable) for sentinel in (*SENTINELS, metric_sentinel, SENTINELS[0]): self.assertNotIn(sentinel, durable) if __name__ == "__main__": unittest.main()