246 lines
9.1 KiB
Python
246 lines
9.1 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import re
|
|
import stat
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
KB_TOOL = ROOT / "hermes-agent" / "leoclean-bin" / "kb_tool.py"
|
|
|
|
|
|
def load_module():
|
|
spec = importlib.util.spec_from_file_location("kb_tool_propose_source", KB_TOOL)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def source_bundle(module) -> dict:
|
|
payload = {
|
|
"apply_payload": {
|
|
"contract_version": 2,
|
|
"claims": [],
|
|
"sources": [],
|
|
"evidence": [],
|
|
"edges": [],
|
|
"reasoning_tools": [],
|
|
}
|
|
}
|
|
payload_sha256 = module.canonical_json_sha256(payload)
|
|
child = {
|
|
"id": "3cf4f2de-ea69-4e3b-a7ec-128b604d4f08",
|
|
"proposal_type": "approve_claim",
|
|
"status": "pending_review",
|
|
"proposed_by_handle": "leo",
|
|
"proposed_by_agent_id": None,
|
|
"channel": "source_document_compiler",
|
|
"source_ref": "normalized:source-parent:0123456789abcdef",
|
|
"rationale": "Strict normalized child of one hash-bound source packet.",
|
|
"payload": payload,
|
|
"payload_sha256": payload_sha256,
|
|
}
|
|
bundle = {
|
|
"schema": module.SOURCE_COMPILER_BUNDLE_SCHEMA,
|
|
"status": "pending_review",
|
|
"build_only": True,
|
|
"database_write_performed": False,
|
|
"canonical_apply_performed": False,
|
|
"strict_child_proposal": child,
|
|
"validated_manifest": {
|
|
"artifact_sha256": "a" * 64,
|
|
"extracted_text_sha256": "b" * 64,
|
|
"source": {
|
|
"identity": "document:sha256:" + "a" * 64,
|
|
"locator": "artifact://sha256/" + "a" * 64,
|
|
}
|
|
},
|
|
"hashes": {
|
|
"artifact_sha256": "a" * 64,
|
|
"extracted_text_sha256": "b" * 64,
|
|
"manifest_canonical_sha256": "c" * 64,
|
|
"strict_child_payload_sha256": payload_sha256,
|
|
},
|
|
}
|
|
bundle["hashes"]["bundle_content_sha256"] = module.canonical_json_sha256(bundle)
|
|
return bundle
|
|
|
|
|
|
def status_row(proposal_count: int) -> dict:
|
|
return {
|
|
"artifact": "teleo_kb_status",
|
|
"backend": "teleo|postgres",
|
|
"high_signal_rows": {
|
|
"claims": 1837,
|
|
"sources": 4145,
|
|
"claim_edges": 4916,
|
|
"claim_evidence": 4670,
|
|
"kb_proposals": proposal_count,
|
|
},
|
|
"proposal_status_counts": {"pending_review": 14},
|
|
}
|
|
|
|
|
|
def test_propose_source_stages_only_pending_review_and_writes_private_receipt(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
module = load_module()
|
|
bundle = source_bundle(module)
|
|
child = bundle["strict_child_proposal"]
|
|
compiler = tmp_path / "compile_kb_source_packet.py"
|
|
compiler.write_text("# compiler fixture\n", encoding="utf-8")
|
|
monkeypatch.setattr(module, "SOURCE_COMPILER_PATH", compiler)
|
|
monkeypatch.setattr(module, "compile_source_bundle", lambda _args: bundle)
|
|
|
|
calls: list[str] = []
|
|
status_reads = iter((status_row(26), status_row(27)))
|
|
|
|
def fake_psql_json(_args, sql):
|
|
calls.append(sql)
|
|
if "insert into kb_stage.kb_proposals" not in sql:
|
|
return [next(status_reads)]
|
|
return [
|
|
{
|
|
"created": True,
|
|
"proposal_id": child["id"],
|
|
"proposal_type": child["proposal_type"],
|
|
"status": "pending_review",
|
|
"source_ref": child["source_ref"],
|
|
"payload": child["payload"],
|
|
"payload_sha256": child["payload_sha256"],
|
|
"created_at": "2026-07-13T18:00:00+00:00",
|
|
"reviewed_at": None,
|
|
"applied_at": None,
|
|
}
|
|
]
|
|
|
|
monkeypatch.setattr(module, "psql_json", fake_psql_json)
|
|
receipt_path = tmp_path / "receipts" / "source.json"
|
|
args = SimpleNamespace(
|
|
local=True,
|
|
artifact=tmp_path / "source.pdf",
|
|
text=tmp_path / "source.txt",
|
|
manifest=tmp_path / "source-manifest.json",
|
|
receipt=receipt_path,
|
|
)
|
|
|
|
receipt = module.propose_source(args)
|
|
|
|
assert receipt["status"] == "pending_review"
|
|
assert receipt["created"] is True
|
|
assert receipt["proposal"]["id"] == child["id"]
|
|
assert receipt["database"]["canonical_counts_unchanged"] is True
|
|
assert receipt["database"]["proposal_count_delta"] == 1
|
|
assert receipt["database"]["proposal_count_matches_command"] is True
|
|
assert receipt["write_contract"] == {
|
|
"target_table": "kb_stage.kb_proposals",
|
|
"database_write_performed_by_command": True,
|
|
"canonical_apply_performed": False,
|
|
"production_db_apply_ran": False,
|
|
"public_table_write_path_present": False,
|
|
}
|
|
assert json.loads(receipt_path.read_text(encoding="utf-8")) == receipt
|
|
assert stat.S_IMODE(receipt_path.stat().st_mode) == 0o600
|
|
|
|
stage_sql = next(sql for sql in calls if "insert into kb_stage.kb_proposals" in sql)
|
|
assert "on conflict (id) do nothing" in stage_sql
|
|
assert "marker_count <> 1 or exact_count <> 1" in stage_sql
|
|
assert not re.search(r"\b(?:insert|update|delete)\s+(?:into\s+|from\s+)?public\.", stage_sql, re.I)
|
|
assert len(calls) == 3
|
|
|
|
|
|
def test_source_bundle_rejects_tampered_child_payload() -> None:
|
|
module = load_module()
|
|
bundle = source_bundle(module)
|
|
bundle["strict_child_proposal"]["payload"]["apply_payload"]["claims"].append({"id": "tampered"})
|
|
|
|
with pytest.raises(SystemExit, match="payload hash does not match"):
|
|
module._validated_source_bundle(bundle)
|
|
|
|
|
|
def test_deployed_bridge_accepts_real_source_compiler_bundle(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
module = load_module()
|
|
artifact_bytes = b"%PDF-1.7\nbridge compatibility fixture\n%%EOF\n"
|
|
extracted_text = (
|
|
"Bridge compatibility source.\n\n"
|
|
"Prepared source inputs may become a pending review proposal.\n\n"
|
|
"Canonical rows still require review and guarded apply."
|
|
)
|
|
artifact = tmp_path / "source.pdf"
|
|
text = tmp_path / "source.txt"
|
|
manifest_path = tmp_path / "source-manifest.json"
|
|
artifact.write_bytes(artifact_bytes)
|
|
text.write_text(extracted_text, encoding="utf-8")
|
|
artifact_sha256 = hashlib.sha256(artifact_bytes).hexdigest()
|
|
text_sha256 = hashlib.sha256(extracted_text.encode()).hexdigest()
|
|
manifest = {
|
|
"schema": "livingip.kbSourceExtractionManifest.v1",
|
|
"artifact_sha256": artifact_sha256,
|
|
"extracted_text_sha256": text_sha256,
|
|
"extractor": {"name": "bridge-test", "version": "1"},
|
|
"source": {
|
|
"identity": f"document:sha256:{artifact_sha256}",
|
|
"source_key": "bridge_compatibility_source",
|
|
"source_type": "article",
|
|
"title": "Bridge compatibility source",
|
|
"locator": f"artifact://sha256/{artifact_sha256}",
|
|
},
|
|
"claims": [
|
|
{
|
|
"claim_key": "prepared_input_staging",
|
|
"type": "structural",
|
|
"text": "Prepared inputs can be staged for review without becoming canonical.",
|
|
"quote": "Prepared source inputs may become a pending review proposal.",
|
|
"confidence": 0.99,
|
|
"tags": ["source-intake"],
|
|
"evidence": [
|
|
{
|
|
"quote": "Canonical rows still require review and guarded apply.",
|
|
"role": "grounds",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
monkeypatch.setattr(module, "SOURCE_COMPILER_PATH", ROOT / "scripts" / "compile_kb_source_packet.py")
|
|
|
|
bundle = module.compile_source_bundle(SimpleNamespace(artifact=artifact, text=text, manifest=manifest_path))
|
|
child, validated_manifest = module._validated_source_bundle(bundle)
|
|
|
|
assert child["status"] == "pending_review"
|
|
assert child["proposal_type"] == "approve_claim"
|
|
assert validated_manifest["artifact_sha256"] == artifact_sha256
|
|
|
|
|
|
def test_propose_source_refuses_nonlocal_database_route(tmp_path: Path) -> None:
|
|
module = load_module()
|
|
args = SimpleNamespace(
|
|
local=False,
|
|
artifact=tmp_path / "source.pdf",
|
|
text=tmp_path / "source.txt",
|
|
manifest=tmp_path / "source-manifest.json",
|
|
receipt=tmp_path / "receipt.json",
|
|
)
|
|
|
|
with pytest.raises(SystemExit, match="VPS-local only"):
|
|
module.propose_source(args)
|
|
|
|
|
|
def test_source_stage_sql_allows_idempotent_readback_after_review_state_advances() -> None:
|
|
module = load_module()
|
|
child, _manifest = module._validated_source_bundle(source_bundle(module))
|
|
|
|
sql = module.build_source_stage_sql(child)
|
|
|
|
assert "on conflict (id) do nothing" in sql
|
|
exact_clause = sql.split("select count(*) into exact_count", 1)[1].split("if marker_count", 1)[0]
|
|
assert "status = 'pending_review'" not in exact_clause
|
|
assert "reviewed_at is null" not in exact_clause
|
|
assert "applied_at is null" not in exact_clause
|