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": "4.0.0", "delivered_processor_version": "4.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": "4.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-4") 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") def test_open_price_review_is_active_and_has_no_terminal_failure(self) -> None: updated = self.started + timedelta(seconds=12) trace = build_job_trace( { "job_id": "arrjob-review-001", "run_status": "awaiting_review", "public_status": "needs_review", "delivery_mode": "artifact_callback", "business_date": "2026-07-29", "created_at": self.started, "updated_at": updated, "filename": "source.xml", "byte_size": 512, "source_sha256": "e" * 64, }, attempts=[ { "attempt_no": 1, "attempt_status": "review_required", "remote_run_id": None, "created_at": self.started + timedelta(seconds=1), "started_at": self.started + timedelta(seconds=2), "finished_at": self.started + timedelta(seconds=7), } ], deliveries=[ { "delivery_key": "local-arrjob-review-001-a1", "attempt_no": 1, "delivery_status": "recorded_review", "result_status": "review_required", "processor_version": "4.0.0", "business_date": "2026-07-29", "daily_version_id": None, "received_at": self.started + timedelta(seconds=3), "validated_at": self.started + timedelta(seconds=5), "committed_at": self.started + timedelta(seconds=7), } ], review_case={ "case_id": "dailyreview-0123456789abcdef0123456789abcdef", "case_status": "open", "revision": 2, "completed_items": 1, "total_items": 2, "manual_override_sha256": None, "created_at": self.started + timedelta(seconds=7), "updated_at": updated, "frozen_at": None, "completed_at": None, "cancelled_at": None, }, review_events=[ { "event_type": "PRICE_REVIEW_REQUIRED", "actor_username": "operator@example.test", "revision": 1, "created_at": self.started + timedelta(seconds=7), }, { "event_type": "PRICE_REVIEW_UPDATED", "actor_username": "operator@example.test", "revision": 2, "created_at": updated, }, ], refreshed_at=updated, ) self.assertEqual(trace["trace_version"], "arr-job-trace-4") self.assertEqual(trace["job"]["status"], "needs_review") self.assertTrue(trace["job"]["active"]) self.assertEqual(trace["job"]["current_stage"], "review") self.assertIsNone(trace["job"]["failure"]) self.assertEqual(trace["job"]["review"]["revision"], 2) self.assertEqual(trace["job"]["review"]["completed_items"], 1) self.assertEqual(trace["job"]["review"]["total_items"], 2) codes = [item["code"] for item in trace["logs"]] self.assertIn("ARTIFACT_RESULT_RECORDED_FOR_REVIEW", codes) self.assertIn("PRICE_REVIEW_REQUIRED", codes) self.assertIn("PRICE_REVIEW_UPDATED", codes) self.assertIn("CURRENT_AWAITING_REVIEW", codes) self.assertNotIn("DOWNSTREAM_EVENT_QUEUED", codes) self.assertNotIn("PRICE_REVIEW_FAILED", codes) rendered = json.dumps(trace, ensure_ascii=False) for private_value in ("SYNTHETIC GUEST", "RES_COMMENT", "TRACE_TEXT"): self.assertNotIn(private_value, rendered) if __name__ == "__main__": unittest.main(verbosity=2)