39 lines
17 KiB
JSON
39 lines
17 KiB
JSON
{
|
|
"schema_version": 1,
|
|
"channel": "homelab-runtime",
|
|
"command_id": "SIGNALBOT-260921-ORDER-NORMALIZATION-TEST-RCA-052FIXRCA1",
|
|
"status": "OK",
|
|
"rc": 0,
|
|
"host": "pve01",
|
|
"mode": "read-only",
|
|
"component": "signalbot-order-normalization-test-rca",
|
|
"started_at_utc": "2026-09-21T03:49:02Z",
|
|
"finished_at_utc": "2026-09-21T03:49:02Z",
|
|
"reference_register_checked": true,
|
|
"reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66",
|
|
"error_register_checked": true,
|
|
"error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0",
|
|
"command_sha256": "bb6b4fbaa3c7a735965adc6a9e217dc7242ed3097f157c63cb7d11fcba77a0fa",
|
|
"duplicate_failed_command_blocked": false,
|
|
"block_reason": null,
|
|
"execution_started": true,
|
|
"change_declared": false,
|
|
"result_contract_valid": true,
|
|
"result_contract_status": null,
|
|
"result_contract_error": null,
|
|
"command_rc": 0,
|
|
"changes_made": false,
|
|
"rollback_started": false,
|
|
"rollback_restored": null,
|
|
"mutation_outcome": "NO_MUTATION",
|
|
"sanitized": true,
|
|
"secrets_included": false,
|
|
"private_addresses_included": false,
|
|
"raw_evidence_retained_locally": true,
|
|
"raw_evidence_sha256": "6f146b4f6887dfacff94c79715d1e60e5cdb633c76d4b25a3f28feec05a11eab",
|
|
"sanitized_output_sha256": "6f146b4f6887dfacff94c79715d1e60e5cdb633c76d4b25a3f28feec05a11eab",
|
|
"output_truncated_in_json": false,
|
|
"full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt",
|
|
"output": "SIGNALBOT052FIXRCA1_RESULT={\"branch\":\"signalbot-260917-reconnect-reconciliation\",\"clean\":true,\"diff_empty\":true,\"head\":\"749073ae0f894debc06fb0ffbf7a84abf9da531f\",\"package_identity\":\"9dd3a15343a59007aa1c0484f9a52b5987c67478687f1a5d3e3c91c7b95a9073\",\"restored_exact\":true,\"source_lines_146_215\":\"146:\\n147: def fetch_trade_window(\\n148: self,\\n149: symbol: str,\\n150: *,\\n151: start_mts: datetime,\\n152: end_mts: datetime,\\n153: ) -> BitfinexTradeHistoryBatch:\\n154: if not isinstance(symbol, str) or not symbol.startswith(\\\"t\\\") or len(symbol) < 2:\\n155: raise DemonovError(\\\"Bitfinex trade history symbol invalid\\\")\\n156: start_mts = _utc(start_mts, \\\"start_mts\\\")\\n157: end_mts = _utc(end_mts, \\\"end_mts\\\")\\n158: if end_mts < start_mts:\\n159: raise DemonovError(\\\"Bitfinex trade history interval invalid\\\")\\n160: start_ms = int(start_mts.timestamp() * 1000)\\n161: end_ms = int(end_mts.timestamp() * 1000)\\n162: query = urlencode({\\\"start\\\": start_ms, \\\"end\\\": end_ms, \\\"sort\\\": 1, \\\"limit\\\": BITFINEX_PUBLIC_HISTORY_LIMIT})\\n163: url = f\\\"{BITFINEX_PUBLIC_TRADES_BASE}/{quote(symbol, safe='')}/hist?{query}\\\"\\n164: try:\\n165: payload = self._fetch_json(\\n166: url,\\n167: timeout=self.timeout,\\n168: max_bytes=self.max_bytes,\\n169: )\\n170: except DemonovError:\\n171: raise\\n172: except Exception as exc:\\n173: raise DemonovError(\\n174: \\\"Bitfinex public trade history request failed\\\"\\n175: ) from exc\\n176: if not isinstance(payload, list):\\n177: raise DemonovError(\\\"Bitfinex public trade history payload invalid\\\")\\n178:\\n179: normalized: list[BitfinexCanonicalTradeV1] = []\\n180: raw_rows: list[list[object]] = []\\n181: seen: dict[str, str] = {}\\n182: order_keys: list[tuple[datetime, int]] = []\\n183: for raw in payload:\\n184: if not isinstance(raw, list):\\n185: raise DemonovError(\\\"Bitfinex REST trade row malformed\\\")\\n186: trade = normalize_bitfinex_rest_trade(raw)\\n187: raw_rows.append(list(raw[:4]))\\n188: prior = seen.get(trade.trade_identity)\\n189: if prior is not None:\\n190: if prior != trade.semantic_hash:\\n191: raise DemonovError(\\\"Bitfinex REST trade semantic conflict\\\")\\n192: continue\\n193: seen[trade.trade_identity] = trade.semantic_hash\\n194: normalized.append(trade)\\n195: order_keys.append((trade.mts, trade.trade_id))\\n196: if order_keys != sorted(order_keys):\\n197: raise DemonovError(\\\"Bitfinex public trade history not ascending\\\")\\n198:\\n199: evidence_sha256 = canonical_hash(\\n200: {\\n201: \\\"schema\\\": \\\"CR0116_BITFINEX_RECONCILIATION_REST_EVIDENCE_V1\\\",\\n202: \\\"symbol\\\": symbol,\\n203: \\\"start_mts\\\": start_mts,\\n204: \\\"end_mts\\\": end_mts,\\n205: \\\"raw_row_count\\\": len(payload),\\n206: \\\"rows\\\": raw_rows,\\n207: }\\n208: )\\n209: return BitfinexTradeHistoryBatch(\\n210: symbol=symbol,\\n211: start_mts=start_mts,\\n212: end_mts=end_mts,\\n213: raw_row_count=len(payload),\\n214: trades=tuple(normalized),\\n215: evidence_sha256=evidence_sha256,\",\"source_sha256\":\"023ce0a041b50b866a48d9f752bd80479a34789d82f73c81d18246ed8871ef8e\",\"test_hits\":[\"126: journal.close()\\n127:\\n128:\\n129:def test_public_history_fetch_is_ascending_inclusive_and_deduplicates_exact_rows():\\n130: a, b, c = _trade(2001, 1), _trade(2002, 2), _trade(2003, 3)\\n131: calls = []\\n132: def fetch(url, *, timeout, max_bytes):\\n133: calls.append((url, timeout, max_bytes))\",\"133: calls.append((url, timeout, max_bytes))\\n134: return [_rest_row(a), _rest_row(b), _rest_row(b), _rest_row(c)]\\n135: client = BitfinexPublicTradeHistoryClient(fetch_json=fetch)\\n136: batch = client.fetch_trade_window(\\\"tBTCUSD\\\", start_mts=a.mts, end_mts=c.mts)\\n137: assert [t.trade_id for t in batch.trades] == [2001, 2002, 2003]\\n138: assert batch.raw_row_count == 4\\n139: assert \\\"sort=1\\\" in calls[0][0] and \\\"limit=10000\\\" in calls[0][0]\\n140: assert f\\\"start={int(a.mts.timestamp()*1000)}\\\" in calls[0][0]\",\"145: a, b = _trade(2051, 1), _trade(2052, 2)\\n146: left = BitfinexPublicTradeHistoryClient(\\n147: fetch_json=lambda *args, **kwargs: [_rest_row(a), _rest_row(a), _rest_row(b)]\\n148: ).fetch_trade_window(\\\"tBTCUSD\\\", start_mts=a.mts, end_mts=b.mts)\\n149: right = BitfinexPublicTradeHistoryClient(\\n150: fetch_json=lambda *args, **kwargs: [_rest_row(a), _rest_row(b), _rest_row(b)]\\n151: ).fetch_trade_window(\\\"tBTCUSD\\\", start_mts=a.mts, end_mts=b.mts)\\n152: assert [t.trade_id for t in left.trades] == [2051, 2052]\",\"148: ).fetch_trade_window(\\\"tBTCUSD\\\", start_mts=a.mts, end_mts=b.mts)\\n149: right = BitfinexPublicTradeHistoryClient(\\n150: fetch_json=lambda *args, **kwargs: [_rest_row(a), _rest_row(b), _rest_row(b)]\\n151: ).fetch_trade_window(\\\"tBTCUSD\\\", start_mts=a.mts, end_mts=b.mts)\\n152: assert [t.trade_id for t in left.trades] == [2051, 2052]\\n153: assert [t.trade_id for t in right.trades] == [2051, 2052]\\n154: assert left.raw_row_count == right.raw_row_count == 3\\n155: assert left.evidence_sha256 != right.evidence_sha256\",\"164: ]\\n165: for client in bad_clients:\\n166: with pytest.raises(DemonovError):\\n167: client.fetch_trade_window(\\\"tBTCUSD\\\", start_mts=a.mts, end_mts=b.mts)\\n168:\\n169:\\n170:def test_successful_reconciliation_recovers_middle_trade_and_is_idempotent(tmp_path):\\n171: journal, window, previous, next_trade = _journal_with_window(tmp_path)\",\"175: calls.append(url)\\n176: return [_rest_row(previous), _rest_row(middle), _rest_row(next_trade)]\\n177: client = BitfinexPublicTradeHistoryClient(fetch_json=fetch)\\n178: resolved = client.reconcile_window(\\n179: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n180: )\\n181: repeated = client.reconcile_window(\\n182: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=21)\",\"178: resolved = client.reconcile_window(\\n179: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n180: )\\n181: repeated = client.reconcile_window(\\n182: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=21)\\n183: )\\n184: assert resolved.status is ReconciliationStatus.RECONCILED\\n185: assert resolved.recovered_row_count == 1\",\"189: journal.close()\\n190:\\n191:\\n192:def test_http_or_validation_failure_marks_window_unresolved(tmp_path):\\n193: journal, window, _, _ = _journal_with_window(tmp_path)\\n194: client = BitfinexPublicTradeHistoryClient(\\n195: fetch_json=lambda *a, **k: (_ for _ in ()).throw(TimeoutError(\\\"simulated\\\"))\\n196: )\",\"194: client = BitfinexPublicTradeHistoryClient(\\n195: fetch_json=lambda *a, **k: (_ for _ in ()).throw(TimeoutError(\\\"simulated\\\"))\\n196: )\\n197: resolved = client.reconcile_window(\\n198: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n199: )\\n200: assert resolved.status is ReconciliationStatus.UNRESOLVED\\n201: assert resolved.failure_reason == \\\"REST_FETCH_OR_VALIDATION_FAILED\\\"\",\"198: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n199: )\\n200: assert resolved.status is ReconciliationStatus.UNRESOLVED\\n201: assert resolved.failure_reason == \\\"REST_FETCH_OR_VALIDATION_FAILED\\\"\\n202: journal.close()\\n203:\\n204:\\n205:def test_exact_row_limit_marks_window_unresolved(tmp_path):\",\"212: rows[-1] = _rest_row(next_trade)\\n213: rows.sort(key=lambda r: (r[1], r[0]))\\n214: client = BitfinexPublicTradeHistoryClient(fetch_json=lambda *a, **k: rows)\\n215: resolved = client.reconcile_window(\\n216: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n217: )\\n218: assert resolved.status is ReconciliationStatus.UNRESOLVED\\n219: assert resolved.failure_reason == \\\"REST_ROW_LIMIT_REACHED\\\"\",\"226: client = BitfinexPublicTradeHistoryClient(\\n227: fetch_json=lambda *a, **k: [_rest_row(previous), _rest_row(middle)]\\n228: )\\n229: resolved = client.reconcile_window(\\n230: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n231: )\\n232: assert resolved.status is ReconciliationStatus.UNRESOLVED\\n233: assert resolved.failure_reason == \\\"NEXT_BOUNDARY_MISSING\\\"\",\"243: client = BitfinexPublicTradeHistoryClient(\\n244: fetch_json=lambda *a, **k: [conflicting, _rest_row(next_trade)]\\n245: )\\n246: resolved = client.reconcile_window(\\n247: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n248: )\\n249: after = journal._db.execute(\\n250: \\\"SELECT COUNT(*) AS n FROM trade_revisions WHERE trade_identity=?\\\", (previous.trade_identity,)\"],\"test_lines_150_280\":\"150: fetch_json=lambda *args, **kwargs: [_rest_row(a), _rest_row(b), _rest_row(b)]\\n151: ).fetch_trade_window(\\\"tBTCUSD\\\", start_mts=a.mts, end_mts=b.mts)\\n152: assert [t.trade_id for t in left.trades] == [2051, 2052]\\n153: assert [t.trade_id for t in right.trades] == [2051, 2052]\\n154: assert left.raw_row_count == right.raw_row_count == 3\\n155: assert left.evidence_sha256 != right.evidence_sha256\\n156:\\n157:\\n158:def test_public_history_rejects_malformed_unordered_and_conflicting_duplicate_rows():\\n159: a, b = _trade(2101, 1), _trade(2102, 2)\\n160: bad_clients = [\\n161: BitfinexPublicTradeHistoryClient(fetch_json=lambda *a, **k: [[1, 2]]),\\n162: BitfinexPublicTradeHistoryClient(fetch_json=lambda *a, **k: [_rest_row(b), _rest_row(a)]),\\n163: BitfinexPublicTradeHistoryClient(fetch_json=lambda *a, **k: [_rest_row(a), [a.trade_id, int(a.mts.timestamp()*1000), \\\"0.5\\\", \\\"101\\\"]]),\\n164: ]\\n165: for client in bad_clients:\\n166: with pytest.raises(DemonovError):\\n167: client.fetch_trade_window(\\\"tBTCUSD\\\", start_mts=a.mts, end_mts=b.mts)\\n168:\\n169:\\n170:def test_successful_reconciliation_recovers_middle_trade_and_is_idempotent(tmp_path):\\n171: journal, window, previous, next_trade = _journal_with_window(tmp_path)\\n172: middle = _trade(1002, 7)\\n173: calls = []\\n174: def fetch(url, *, timeout, max_bytes):\\n175: calls.append(url)\\n176: return [_rest_row(previous), _rest_row(middle), _rest_row(next_trade)]\\n177: client = BitfinexPublicTradeHistoryClient(fetch_json=fetch)\\n178: resolved = client.reconcile_window(\\n179: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n180: )\\n181: repeated = client.reconcile_window(\\n182: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=21)\\n183: )\\n184: assert resolved.status is ReconciliationStatus.RECONCILED\\n185: assert resolved.recovered_row_count == 1\\n186: assert journal.has_trade_identity(middle.trade_identity)\\n187: assert repeated == resolved\\n188: assert len(calls) == 1\\n189: journal.close()\\n190:\\n191:\\n192:def test_http_or_validation_failure_marks_window_unresolved(tmp_path):\\n193: journal, window, _, _ = _journal_with_window(tmp_path)\\n194: client = BitfinexPublicTradeHistoryClient(\\n195: fetch_json=lambda *a, **k: (_ for _ in ()).throw(TimeoutError(\\\"simulated\\\"))\\n196: )\\n197: resolved = client.reconcile_window(\\n198: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n199: )\\n200: assert resolved.status is ReconciliationStatus.UNRESOLVED\\n201: assert resolved.failure_reason == \\\"REST_FETCH_OR_VALIDATION_FAILED\\\"\\n202: journal.close()\\n203:\\n204:\\n205:def test_exact_row_limit_marks_window_unresolved(tmp_path):\\n206: journal, window, previous, next_trade = _journal_with_window(tmp_path)\\n207: rows = []\\n208: for i in range(BITFINEX_PUBLIC_HISTORY_LIMIT):\\n209: trade_id = 300000 + i\\n210: rows.append([trade_id, int((previous.mts + timedelta(milliseconds=i)).timestamp()*1000), \\\"0.5\\\", \\\"100\\\"])\\n211: rows[0] = _rest_row(previous)\\n212: rows[-1] = _rest_row(next_trade)\\n213: rows.sort(key=lambda r: (r[1], r[0]))\\n214: client = BitfinexPublicTradeHistoryClient(fetch_json=lambda *a, **k: rows)\\n215: resolved = client.reconcile_window(\\n216: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n217: )\\n218: assert resolved.status is ReconciliationStatus.UNRESOLVED\\n219: assert resolved.failure_reason == \\\"REST_ROW_LIMIT_REACHED\\\"\\n220: journal.close()\\n221:\\n222:\\n223:def test_missing_boundary_marks_window_unresolved(tmp_path):\\n224: journal, window, previous, _ = _journal_with_window(tmp_path, persist_next=False)\\n225: middle = _trade(1002, 7)\\n226: client = BitfinexPublicTradeHistoryClient(\\n227: fetch_json=lambda *a, **k: [_rest_row(previous), _rest_row(middle)]\\n228: )\\n229: resolved = client.reconcile_window(\\n230: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n231: )\\n232: assert resolved.status is ReconciliationStatus.UNRESOLVED\\n233: assert resolved.failure_reason == \\\"NEXT_BOUNDARY_MISSING\\\"\\n234: journal.close()\\n235:\\n236:\\n237:def test_semantic_conflict_marks_window_unresolved_without_new_revision(tmp_path):\\n238: journal, window, previous, next_trade = _journal_with_window(tmp_path)\\n239: conflicting = [previous.trade_id, int(previous.mts.timestamp()*1000), \\\"0.5\\\", \\\"101\\\"]\\n240: before = journal._db.execute(\\n241: \\\"SELECT COUNT(*) AS n FROM trade_revisions WHERE trade_identity=?\\\", (previous.trade_identity,)\\n242: ).fetchone()[\\\"n\\\"]\\n243: client = BitfinexPublicTradeHistoryClient(\\n244: fetch_json=lambda *a, **k: [conflicting, _rest_row(next_trade)]\\n245: )\\n246: resolved = client.reconcile_window(\\n247: journal, window.window_id, symbol=\\\"tBTCUSD\\\", resolved_at=T0 + timedelta(seconds=20)\\n248: )\\n249: after = journal._db.execute(\\n250: \\\"SELECT COUNT(*) AS n FROM trade_revisions WHERE trade_identity=?\\\", (previous.trade_identity,)\\n251: ).fetchone()[\\\"n\\\"]\\n252: assert resolved.status is ReconciliationStatus.UNRESOLVED\\n253: assert resolved.failure_reason == \\\"REST_SEMANTIC_CONFLICT\\\"\\n254: assert after == before\\n255: journal.close()\",\"test_sha256\":\"bebdd9c2875d2c63a0ac27d89eefa7f1c36c83c067aa8b472c0616ac470dd2a2\"}\nTASK_RESULT=PASS_SIGNALBOT_052FIXRCA1_ROLLBACK_AND_TEST_SEMANTICS_CAPTURED\nHOMELAB_RESULT_CONTRACT={\"version\":1,\"command_id\":\"SIGNALBOT-260921-ORDER-NORMALIZATION-TEST-RCA-052FIXRCA1\",\"status\":\"OK\",\"changes_made\":false,\"rollback_started\":false,\"rollback_restored\":null}\n"
|
|
}
|