from __future__ import annotations import json import unittest from datetime import datetime, timedelta, timezone from arr_web.job_trace import build_job_trace class JobTraceTests(unittest.TestCase): def setUp(self) -> None: self.started = datetime(2026, 7, 29, 11, 0, tzinfo=timezone.utc) def test_programmatic_success_exposes_persisted_safe_trace(self) -> None: finished = self.started + timedelta(seconds=8) trace = build_job_trace( { "job_id": "arrjob-trace-001", "run_status": "accepted", "public_status": "succeeded", "delivery_mode": "artifact_callback", "business_date": "2026-07-29", "created_at": self.started, "updated_at": finished, "validated_at": finished, "finished_at": finished, "filename": "source.xml", "byte_size": 2048, "source_sha256": "a" * 64, "requested_processor_version": "3.0.0", "delivered_processor_version": "3.0.0", "object_key": "must/not/escape", }, attempts=[ { "attempt_no": 1, "attempt_status": "succeeded", "remote_run_id": None, "created_at": self.started + timedelta(seconds=1), "started_at": self.started + timedelta(seconds=2), "finished_at": finished, } ], deliveries=[ { "delivery_key": "local-arrjob-trace-001-a1", "attempt_no": 1, "delivery_status": "committed", "result_status": "success", "processor_version": "3.0.0", "business_date": "2026-07-29", "daily_version_id": 41, "received_at": self.started + timedelta(seconds=5), "validated_at": self.started + timedelta(seconds=6), "committed_at": finished, } ], versions=[ { "daily_version_id": 41, "business_date": "2026-07-29", "version_no": 2, "version_status": "active", "result_delivery_mode": "artifact_callback", "source_rows": 20, "retained_rows": 17, "excluded_rate_code_rows": 1, "duplicate_rows": 2, "validation_failed_rows": 0, "price_unmatched_rows": 0, "created_at": self.started + timedelta(seconds=7), "validated_at": finished, "activated_at": finished, } ], outbox_events=[ { "event_key": "processing-run:1:accepted", "event_type": "arr.daily_version_committed", "publish_status": "pending", "publish_attempts": 0, "created_at": finished, "available_at": finished, "payload": {"must": "not escape"}, } ], refreshed_at=finished + timedelta(seconds=1), ) self.assertEqual(trace["trace_version"], "arr-job-trace-3") self.assertEqual(trace["job"]["status"], "succeeded") self.assertEqual(trace["job"]["current_stage"], "downstream") self.assertFalse(trace["job"]["active"]) self.assertIsNone(trace["job"]["remote_run_id"]) self.assertEqual(trace["job"]["execution_scope"], "arr_runtime") self.assertEqual(trace["job"]["processor_mode"], "fixed_processor") self.assertEqual(trace["job"]["remote_dispatch"], "none") self.assertEqual(trace["job"]["version_no"], 2) self.assertEqual(trace["job"]["output_rows"], 17) codes = [item["code"] for item in trace["logs"]] for expected in ( "UPLOAD_REGISTERED", "ATTEMPT_QUEUED", "PROCESSOR_STARTED", "ARTIFACT_RESULT_RECEIVED", "ARTIFACT_RESULT_VALIDATED", "ARTIFACT_RESULT_COMMITTED", "FINANCE_VERSION_VALIDATED", "FINANCE_VERSION_ACTIVATED", "DOWNSTREAM_EVENT_QUEUED", "JOB_SUCCEEDED", ): self.assertIn(expected, codes) self.assertEqual(trace["evidence"]["artifact_deliveries"], 1) rendered = json.dumps(trace, ensure_ascii=False) for private_value in ( "must/not/escape", "must not escape", '"payload"', '"object_key"', "submission_grant", "Agent", "MCP", ): self.assertNotIn(private_value, rendered) self.assertNotIn("本地处理", rendered) self.assertIn("ARR 固定处理器输出", rendered) self.assertIn("Finance 数据库提交已完成", rendered) def test_runtime_failure_is_located_at_processor_boundary(self) -> None: finished = self.started + timedelta(minutes=4) trace = build_job_trace( { "job_id": "arrjob-timeout-001", "run_status": "failed", "public_status": "failed", "delivery_mode": "artifact_callback", "failure_code": "PROCESSOR_TIMEOUT", "failure_message": "programmatic processing failed", "created_at": self.started, "updated_at": finished, "finished_at": finished, "filename": "source.xml", "byte_size": 99, "source_sha256": "b" * 64, }, attempts=[ { "attempt_no": 1, "attempt_status": "failed", "remote_run_id": None, "failure_code": "PROCESSOR_TIMEOUT", "failure_message": "programmatic processing failed", "created_at": self.started + timedelta(seconds=1), "started_at": self.started + timedelta(seconds=2), "finished_at": finished, } ], refreshed_at=finished, ) self.assertEqual(trace["job"]["current_stage"], "processor") self.assertEqual(trace["job"]["failure"]["code"], "PROCESSOR_TIMEOUT") errors = [item for item in trace["logs"] if item["level"] == "error"] self.assertGreaterEqual(len(errors), 2) self.assertTrue(all(item["stage"] == "processor" for item in errors)) def test_legacy_direct_mcp_trace_exposes_remote_semantics(self) -> None: finished = self.started + timedelta(minutes=4) trace = build_job_trace( { "job_id": "arrjob-legacy-001", "run_status": "failed", "public_status": "failed", "delivery_mode": "direct_mcp", "failure_code": "REMOTE_FAILED", "failure_message": "remote processor failed", "created_at": self.started, "updated_at": finished, "finished_at": finished, "filename": "source.xml", "byte_size": 99, "source_sha256": "d" * 64, }, attempts=[ { "attempt_no": 1, "attempt_status": "failed", "remote_run_id": "remote-run-123", "failure_code": "REMOTE_FAILED", "failure_message": "remote processor failed", "created_at": self.started + timedelta(seconds=1), "started_at": self.started + timedelta(seconds=2), "finished_at": finished, } ], refreshed_at=finished, ) self.assertEqual(trace["job"]["execution_scope"], "remote_agent") self.assertEqual(trace["job"]["processor_mode"], "legacy_direct_mcp") self.assertEqual(trace["job"]["remote_dispatch"], "mcp") self.assertEqual(trace["job"]["remote_run_id"], "remote-run-123") def test_active_run_ends_with_current_programmatic_state(self) -> None: trace = build_job_trace( { "job_id": "arrjob-running-001", "run_status": "running", "public_status": "running", "delivery_mode": "artifact_callback", "created_at": self.started, "updated_at": self.started + timedelta(seconds=4), "filename": "source.xml", "byte_size": 128, "source_sha256": "c" * 64, }, attempts=[ { "attempt_no": 1, "attempt_status": "running", "remote_run_id": None, "created_at": self.started + timedelta(seconds=1), "started_at": self.started + timedelta(seconds=2), } ], ) self.assertTrue(trace["job"]["active"]) self.assertEqual(trace["job"]["current_stage"], "processor") self.assertEqual(trace["logs"][-1]["code"], "CURRENT_RUNNING") if __name__ == "__main__": unittest.main(verbosity=2)