|
2 | 2 |
|
3 | 3 | import asyncio |
4 | 4 | import json |
| 5 | +import os |
5 | 6 | import uuid |
6 | 7 |
|
7 | 8 | import pytest |
@@ -515,6 +516,105 @@ async def empty_extract_facts_from_contents( |
515 | 516 | assert "extraction_errors_sample" not in parent["result_metadata"] |
516 | 517 |
|
517 | 518 |
|
| 519 | +async def _seed_retain_op_with_errors(pool, bank_id: str, error_count: int) -> uuid.UUID: |
| 520 | + """Insert a pending retain operation whose outcome metadata records extraction errors.""" |
| 521 | + operation_id = uuid.uuid4() |
| 522 | + await pool.execute( |
| 523 | + """ |
| 524 | + INSERT INTO async_operations (operation_id, bank_id, operation_type, result_metadata, status) |
| 525 | + VALUES ($1, $2, $3, $4, $5) |
| 526 | + """, |
| 527 | + operation_id, |
| 528 | + bank_id, |
| 529 | + "retain", |
| 530 | + json.dumps( |
| 531 | + { |
| 532 | + "unit_ids_count": 3, |
| 533 | + "extraction_errors_count": error_count, |
| 534 | + "extraction_errors_sample": ["chunk 2 failed to parse"], |
| 535 | + } |
| 536 | + ), |
| 537 | + "pending", |
| 538 | + ) |
| 539 | + return operation_id |
| 540 | + |
| 541 | + |
| 542 | +async def _op_row(pool, operation_id: uuid.UUID): |
| 543 | + return await pool.fetchrow( |
| 544 | + "SELECT status, error_message FROM async_operations WHERE operation_id = $1", |
| 545 | + operation_id, |
| 546 | + ) |
| 547 | + |
| 548 | + |
| 549 | +@pytest.mark.asyncio |
| 550 | +async def test_completion_marks_failed_when_flag_on_and_errors_present(memory): |
| 551 | + """With the escape hatch on, a retain that dropped facts ends 'failed' (issue #2700).""" |
| 552 | + from hindsight_api.config import ENV_FAIL_ON_EXTRACTION_ERRORS, clear_config_cache |
| 553 | + |
| 554 | + bank_id = "test_fail_on_extraction_errors_on" |
| 555 | + pool = await memory._get_pool() |
| 556 | + await _ensure_bank(pool, bank_id) |
| 557 | + operation_id = await _seed_retain_op_with_errors(pool, bank_id, error_count=2) |
| 558 | + |
| 559 | + os.environ[ENV_FAIL_ON_EXTRACTION_ERRORS] = "true" |
| 560 | + clear_config_cache() |
| 561 | + try: |
| 562 | + await memory._mark_operation_completed(str(operation_id)) |
| 563 | + finally: |
| 564 | + del os.environ[ENV_FAIL_ON_EXTRACTION_ERRORS] |
| 565 | + clear_config_cache() |
| 566 | + |
| 567 | + row = await _op_row(pool, operation_id) |
| 568 | + assert row["status"] == "failed" |
| 569 | + assert row["error_message"] is not None |
| 570 | + assert "2" in row["error_message"] |
| 571 | + assert "extraction error" in row["error_message"].lower() |
| 572 | + |
| 573 | + |
| 574 | +@pytest.mark.asyncio |
| 575 | +async def test_completion_stays_completed_when_flag_off(memory): |
| 576 | + """Default behavior is preserved: extraction errors still complete the operation.""" |
| 577 | + from hindsight_api.config import ENV_FAIL_ON_EXTRACTION_ERRORS, clear_config_cache |
| 578 | + |
| 579 | + bank_id = "test_fail_on_extraction_errors_off" |
| 580 | + pool = await memory._get_pool() |
| 581 | + await _ensure_bank(pool, bank_id) |
| 582 | + operation_id = await _seed_retain_op_with_errors(pool, bank_id, error_count=2) |
| 583 | + |
| 584 | + os.environ.pop(ENV_FAIL_ON_EXTRACTION_ERRORS, None) |
| 585 | + clear_config_cache() |
| 586 | + try: |
| 587 | + await memory._mark_operation_completed(str(operation_id)) |
| 588 | + finally: |
| 589 | + clear_config_cache() |
| 590 | + |
| 591 | + row = await _op_row(pool, operation_id) |
| 592 | + assert row["status"] == "completed" |
| 593 | + assert row["error_message"] is None |
| 594 | + |
| 595 | + |
| 596 | +@pytest.mark.asyncio |
| 597 | +async def test_completion_completed_when_flag_on_but_no_errors(memory): |
| 598 | + """The flag only fails operations that actually accumulated extraction errors.""" |
| 599 | + from hindsight_api.config import ENV_FAIL_ON_EXTRACTION_ERRORS, clear_config_cache |
| 600 | + |
| 601 | + bank_id = "test_fail_on_extraction_errors_none" |
| 602 | + pool = await memory._get_pool() |
| 603 | + await _ensure_bank(pool, bank_id) |
| 604 | + operation_id = await _seed_retain_op_with_errors(pool, bank_id, error_count=0) |
| 605 | + |
| 606 | + os.environ[ENV_FAIL_ON_EXTRACTION_ERRORS] = "true" |
| 607 | + clear_config_cache() |
| 608 | + try: |
| 609 | + await memory._mark_operation_completed(str(operation_id)) |
| 610 | + finally: |
| 611 | + del os.environ[ENV_FAIL_ON_EXTRACTION_ERRORS] |
| 612 | + clear_config_cache() |
| 613 | + |
| 614 | + row = await _op_row(pool, operation_id) |
| 615 | + assert row["status"] == "completed" |
| 616 | + |
| 617 | + |
518 | 618 | @pytest.mark.asyncio |
519 | 619 | async def test_retain_records_user_provided_document_ids(memory, request_context): |
520 | 620 | """User-supplied document_ids land in child op result_metadata.document_ids.""" |
|
0 commit comments