304 lines
9.6 KiB
Python
304 lines
9.6 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from opensquilla.session.compaction_lifecycle import (
|
|
compaction_memory_status,
|
|
compaction_safety_allows_destructive_compaction,
|
|
durable_receipt_allows_destructive_compaction,
|
|
flush_compaction_decision,
|
|
flush_receipt_allows_destructive_compaction,
|
|
flush_receipt_status_for_compaction,
|
|
flush_trigger_enabled,
|
|
normalize_flush_triggers,
|
|
pre_compaction_flush_enabled,
|
|
)
|
|
|
|
|
|
def _receipt(**overrides):
|
|
payload = {
|
|
"mode": "llm",
|
|
"indexed_chunk_count": 1,
|
|
"integrity_status": "ok",
|
|
"output_coverage_status": "ok",
|
|
"invalid_candidate_count": 0,
|
|
"candidate_missing_ids": [],
|
|
"obligation_status": "ok",
|
|
"obligation_missing_ids": [],
|
|
}
|
|
payload.update(overrides)
|
|
return SimpleNamespace(**payload)
|
|
|
|
|
|
def test_unverifiable_flush_receipt_is_not_destructive_safe() -> None:
|
|
receipt = _receipt(output_coverage_status="unverifiable")
|
|
|
|
assert flush_receipt_allows_destructive_compaction(receipt) is False
|
|
assert flush_compaction_decision(receipt, safety_mode="protect") == "degraded_forensic"
|
|
|
|
|
|
def test_flush_enabled_does_not_imply_pre_compaction_trigger() -> None:
|
|
config = SimpleNamespace(memory=SimpleNamespace(flush_enabled=True))
|
|
|
|
assert flush_trigger_enabled(config, "session_reset") is True
|
|
assert pre_compaction_flush_enabled(config) is False
|
|
|
|
|
|
def test_pre_compaction_flush_accepts_explicit_trigger_or_legacy_flag() -> None:
|
|
trigger_config = SimpleNamespace(
|
|
memory=SimpleNamespace(
|
|
flush_enabled=True,
|
|
flush_triggers=["manual", "pre_compaction"],
|
|
)
|
|
)
|
|
legacy_config = SimpleNamespace(
|
|
memory=SimpleNamespace(
|
|
flush_enabled=True,
|
|
flush_pre_compaction=True,
|
|
)
|
|
)
|
|
|
|
assert pre_compaction_flush_enabled(trigger_config) is True
|
|
assert pre_compaction_flush_enabled(legacy_config) is True
|
|
|
|
|
|
def test_flush_triggers_normalize_aliases_and_strings() -> None:
|
|
assert normalize_flush_triggers("reset, inline_overflow") == frozenset(
|
|
{"session_reset", "pre_compaction"}
|
|
)
|
|
|
|
|
|
def test_backfilled_obligations_remain_destructive_safe() -> None:
|
|
receipt = _receipt(obligation_status="backfilled")
|
|
|
|
assert flush_receipt_allows_destructive_compaction(receipt) is True
|
|
assert flush_compaction_decision(receipt, safety_mode="protect") == "safe_destructive"
|
|
|
|
|
|
def test_checkpoint_receipt_allows_destructive_compaction() -> None:
|
|
receipt = {
|
|
"scope": "checkpoint",
|
|
"status": "checkpoint_saved",
|
|
"source_path": "memory/.checkpoints/agent-main-webchat-abc/turn-1.jsonl",
|
|
"content_hash": "h1",
|
|
}
|
|
|
|
assert durable_receipt_allows_destructive_compaction(receipt) is True
|
|
|
|
|
|
def test_orphaned_checkpoint_receipt_is_not_destructive_safe() -> None:
|
|
receipt = {"scope": "checkpoint", "status": "receipt_orphaned"}
|
|
|
|
assert durable_receipt_allows_destructive_compaction(receipt) is False
|
|
|
|
|
|
def test_checkpoint_failed_receipt_is_not_destructive_safe() -> None:
|
|
receipt = {
|
|
"scope": "checkpoint",
|
|
"status": "checkpoint_failed",
|
|
"source_path": "memory/.checkpoints/agent-main-webchat-abc/turn-1.jsonl",
|
|
"content_hash": "h1",
|
|
}
|
|
|
|
assert durable_receipt_allows_destructive_compaction(receipt) is False
|
|
|
|
|
|
def test_checkpoint_receipt_without_evidence_is_not_destructive_safe() -> None:
|
|
receipt = {
|
|
"scope": "checkpoint",
|
|
"status": "checkpoint_saved",
|
|
"source_path": "memory/.checkpoints/agent-main-webchat-abc/turn-1.jsonl",
|
|
"content_hash": "",
|
|
}
|
|
|
|
assert durable_receipt_allows_destructive_compaction(receipt) is False
|
|
|
|
|
|
def test_missing_or_raw_receipt_enters_degraded_forensic_in_protect_mode() -> None:
|
|
assert flush_compaction_decision(None, safety_mode="protect") == "degraded_forensic"
|
|
assert (
|
|
flush_compaction_decision(
|
|
_receipt(mode="raw", indexed_chunk_count=0),
|
|
safety_mode="protect",
|
|
)
|
|
== "degraded_forensic"
|
|
)
|
|
|
|
|
|
def test_disabled_flush_decision_is_explicit() -> None:
|
|
assert flush_compaction_decision(None, safety_mode="off") == "disabled"
|
|
|
|
|
|
def test_noop_flush_receipt_has_distinct_compaction_status() -> None:
|
|
config = SimpleNamespace(
|
|
memory=SimpleNamespace(
|
|
flush_compaction_safety_mode="protect",
|
|
flush_compaction_requires_safe_receipt=False,
|
|
)
|
|
)
|
|
receipt = _receipt(
|
|
indexed_chunk_count=0,
|
|
integrity_status="unverified",
|
|
output_coverage_status="unverifiable",
|
|
obligation_status="unverifiable",
|
|
result_status="ok_noop_no_memory",
|
|
)
|
|
|
|
assert flush_receipt_allows_destructive_compaction(receipt) is False
|
|
assert flush_receipt_status_for_compaction(receipt, config) == "noop_no_memory"
|
|
|
|
|
|
def test_raw_archive_flush_receipt_has_distinct_compaction_status() -> None:
|
|
config = SimpleNamespace(
|
|
memory=SimpleNamespace(
|
|
flush_compaction_safety_mode="protect",
|
|
flush_compaction_requires_safe_receipt=False,
|
|
)
|
|
)
|
|
receipt = _receipt(
|
|
mode="raw",
|
|
indexed_chunk_count=0,
|
|
integrity_status="unverified",
|
|
output_coverage_status="unverifiable",
|
|
obligation_status="unverifiable",
|
|
result_status="ok_archive_only",
|
|
)
|
|
|
|
assert flush_receipt_allows_destructive_compaction(receipt) is False
|
|
assert flush_receipt_status_for_compaction(receipt, config) == "archive_only"
|
|
|
|
|
|
def test_deterministic_safety_evidence_makes_semantic_flush_failure_non_fatal() -> None:
|
|
receipt = _receipt(
|
|
mode="raw",
|
|
result_status="parse_failed_archived",
|
|
flushed_paths=["memory/.raw_fallbacks/raw.md"],
|
|
content_hash="h1",
|
|
indexed_chunk_count=0,
|
|
integrity_status="unverified",
|
|
output_coverage_status="unverified",
|
|
obligation_status="unverified",
|
|
)
|
|
|
|
status = compaction_memory_status(
|
|
receipt,
|
|
deterministic_receipt_safe=True,
|
|
)
|
|
|
|
assert status.safety_status == "safe"
|
|
assert status.semantic_status == "failed"
|
|
assert status.allows_destructive_compaction is True
|
|
|
|
|
|
def test_raw_archive_receipt_is_degraded_archive_safety_evidence() -> None:
|
|
receipt = _receipt(
|
|
mode="raw",
|
|
result_status="ok_archive_only",
|
|
flushed_paths=["memory/.raw_fallbacks/raw.md"],
|
|
content_hash="h1",
|
|
indexed_chunk_count=0,
|
|
integrity_status="unverified",
|
|
output_coverage_status="unverified",
|
|
obligation_status="unverified",
|
|
)
|
|
|
|
status = compaction_memory_status(receipt)
|
|
|
|
assert status.safety_status == "degraded_archive"
|
|
assert status.semantic_status == "degraded"
|
|
assert status.allows_destructive_compaction is True
|
|
assert compaction_safety_allows_destructive_compaction(receipt) is True
|
|
|
|
|
|
def test_raw_archive_receipt_without_content_hash_is_unsafe() -> None:
|
|
receipt = _receipt(
|
|
mode="raw",
|
|
result_status="ok_archive_only",
|
|
flushed_paths=["memory/.raw_fallbacks/raw.md"],
|
|
content_hash="",
|
|
indexed_chunk_count=0,
|
|
integrity_status="unverified",
|
|
output_coverage_status="unverified",
|
|
obligation_status="unverified",
|
|
)
|
|
|
|
status = compaction_memory_status(receipt)
|
|
|
|
assert status.safety_status == "unsafe"
|
|
assert status.semantic_status == "degraded"
|
|
assert status.allows_destructive_compaction is False
|
|
assert compaction_safety_allows_destructive_compaction(receipt) is False
|
|
|
|
|
|
def test_raw_archive_receipt_accepts_string_flushed_path() -> None:
|
|
receipt = _receipt(
|
|
mode="raw",
|
|
result_status="ok_archive_only",
|
|
flushed_paths="memory/.raw_fallbacks/raw.md",
|
|
content_hash="h1",
|
|
indexed_chunk_count=0,
|
|
integrity_status="unverified",
|
|
output_coverage_status="unverified",
|
|
obligation_status="unverified",
|
|
)
|
|
|
|
status = compaction_memory_status(receipt)
|
|
|
|
assert status.safety_status == "degraded_archive"
|
|
assert status.semantic_status == "degraded"
|
|
assert status.allows_destructive_compaction is True
|
|
assert compaction_safety_allows_destructive_compaction(receipt) is True
|
|
|
|
|
|
def test_archive_failed_without_deterministic_evidence_is_unsafe() -> None:
|
|
receipt = _receipt(
|
|
mode="error",
|
|
result_status="archive_failed",
|
|
flushed_paths=[],
|
|
content_hash="h1",
|
|
indexed_chunk_count=0,
|
|
integrity_status="unverified",
|
|
output_coverage_status="unverified",
|
|
obligation_status="unverified",
|
|
)
|
|
|
|
status = compaction_memory_status(receipt)
|
|
|
|
assert status.safety_status == "unsafe"
|
|
assert status.semantic_status == "failed"
|
|
assert status.allows_destructive_compaction is False
|
|
|
|
|
|
def test_durable_preimage_receipt_allows_destructive_compaction() -> None:
|
|
receipt = _receipt(
|
|
scope="preimage",
|
|
status="preimage_saved",
|
|
target_path="memory/.raw_fallbacks/raw.md",
|
|
content_hash="h1",
|
|
)
|
|
|
|
assert durable_receipt_allows_destructive_compaction(receipt) is True
|
|
|
|
|
|
def test_durable_repair_pending_archive_receipt_allows_destructive_compaction() -> None:
|
|
receipt = _receipt(
|
|
scope="repair",
|
|
status="repair_pending",
|
|
reason="apply_failed_archived",
|
|
target_path="memory/.raw_fallbacks/raw.md",
|
|
content_hash="h1",
|
|
)
|
|
|
|
assert durable_receipt_allows_destructive_compaction(receipt) is True
|
|
|
|
|
|
def test_durable_preimage_receipt_without_archive_hash_is_unsafe() -> None:
|
|
receipt = _receipt(
|
|
scope="preimage",
|
|
status="preimage_saved",
|
|
target_path="memory/.raw_fallbacks/raw.md",
|
|
content_hash="",
|
|
)
|
|
|
|
assert durable_receipt_allows_destructive_compaction(receipt) is False
|