162 lines
5.7 KiB
Python
162 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
from scripts import leo_tool_trace
|
|
from scripts.run_gcp_generated_db_blind_claim_canary import (
|
|
DISCOVERY_SUBCOMMANDS,
|
|
EXPECTED_CLAIM_ID,
|
|
EXPECTED_SOURCE_IDS,
|
|
EXPECTED_SUBCOMMANDS,
|
|
PROMPT,
|
|
reply_outcomes,
|
|
sanitize_gateway,
|
|
should_reexec_in_hermes,
|
|
summarize_wrapper_tool_proof,
|
|
trace_facts,
|
|
transcript_events_to_messages,
|
|
)
|
|
|
|
|
|
def retrieval_content(*row_ids: str) -> str:
|
|
return "\n".join(
|
|
[
|
|
"schema: livingip.teleoKbRetrievalReceipt.v1",
|
|
f"semantic context SHA-256: `{'1' * 64}`",
|
|
f"artifact state SHA-256: `{'2' * 64}`",
|
|
"DB read consistency: `stable_wal_marker`",
|
|
*row_ids,
|
|
]
|
|
)
|
|
|
|
|
|
def test_blind_prompt_contains_no_row_identifier() -> None:
|
|
assert EXPECTED_CLAIM_ID not in PROMPT
|
|
assert not EXPECTED_SOURCE_IDS.intersection(PROMPT.split())
|
|
|
|
|
|
def test_blind_runner_reexecs_when_venv_python_symlinks_to_system_python(tmp_path) -> None:
|
|
system_python = tmp_path / "system-python"
|
|
system_python.touch()
|
|
hermes_python = tmp_path / "hermes-python"
|
|
hermes_python.symlink_to(system_python)
|
|
|
|
assert should_reexec_in_hermes(system_python, hermes_python) is True
|
|
assert should_reexec_in_hermes(hermes_python, hermes_python) is False
|
|
|
|
|
|
def test_transcript_proves_three_receipted_read_calls_and_expected_rows() -> None:
|
|
source_ids = sorted(EXPECTED_SOURCE_IDS)
|
|
events = []
|
|
for index, (subcommand, content) in enumerate(
|
|
(
|
|
("search", retrieval_content(EXPECTED_CLAIM_ID)),
|
|
("show", retrieval_content(EXPECTED_CLAIM_ID)),
|
|
("evidence", retrieval_content(EXPECTED_CLAIM_ID, *source_ids)),
|
|
)
|
|
):
|
|
call_id = f"call-{index}"
|
|
events.extend(
|
|
[
|
|
{
|
|
"phase": "call",
|
|
"tool_call_id": call_id,
|
|
"tool_name": "terminal",
|
|
"arguments": {"command": f"teleo-kb {subcommand} query"},
|
|
},
|
|
{
|
|
"phase": "result",
|
|
"tool_call_id": call_id,
|
|
"tool_name": "terminal",
|
|
"content": content,
|
|
},
|
|
]
|
|
)
|
|
|
|
trace = leo_tool_trace.extract_kb_tool_trace(transcript_events_to_messages(events))
|
|
subcommands, row_ids, receipt_count = trace_facts(trace)
|
|
|
|
assert subcommands >= EXPECTED_SUBCOMMANDS
|
|
assert subcommands & DISCOVERY_SUBCOMMANDS
|
|
assert EXPECTED_CLAIM_ID in row_ids
|
|
assert row_ids >= EXPECTED_SOURCE_IDS
|
|
assert receipt_count == 3
|
|
assert trace["database_tool_calls_read_only"] is True
|
|
|
|
|
|
def test_gateway_sanitization_hashes_events_without_retaining_them() -> None:
|
|
secret = "raw-secret-that-must-not-survive"
|
|
sanitized = sanitize_gateway(
|
|
{
|
|
"reply": "safe reply",
|
|
"transcript_tool_trace": {
|
|
"event_count": 1,
|
|
"events": [{"content": secret}],
|
|
},
|
|
}
|
|
)
|
|
|
|
assert sanitized["reply"] == "safe reply"
|
|
assert sanitized["transcript_tool_trace"]["event_count"] == 1
|
|
assert sanitized["transcript_tool_trace"]["raw_events_retained"] is False
|
|
assert len(sanitized["transcript_tool_trace"]["events_sha256"]) == 64
|
|
assert secret not in str(sanitized)
|
|
|
|
|
|
def test_wrapper_summary_drops_raw_arguments_but_keeps_database_binding() -> None:
|
|
secret_argument = "do-not-retain-this-query"
|
|
summarized = summarize_wrapper_tool_proof(
|
|
{
|
|
"event_count": 2,
|
|
"invocation_count": 1,
|
|
"complete_start_end_pairing": True,
|
|
"all_bound_to_supplied_target": True,
|
|
"database_read_only_required": True,
|
|
"default_read_only_required": True,
|
|
"all_completed_successfully": True,
|
|
"invocations": [
|
|
{
|
|
"argv": ["search", secret_argument],
|
|
"container": "teleo-prod-1",
|
|
"database": "teleo_clone_test",
|
|
"database_identity": {"ssl": True},
|
|
"returncode": 0,
|
|
}
|
|
],
|
|
}
|
|
)
|
|
|
|
assert summarized["invocations"][0]["subcommand"] == "search"
|
|
assert summarized["invocations"][0]["argument_count"] == 1
|
|
assert summarized["invocations"][0]["database"] == "teleo_clone_test"
|
|
assert summarized["raw_argv_retained"] is False
|
|
assert secret_argument not in str(summarized)
|
|
|
|
|
|
def test_reply_outcomes_accepts_reasoned_alternative_decomposition() -> None:
|
|
reply = """
|
|
The support is thin: the two sources do not establish the full legal conclusion.
|
|
I would split it into candidate claims about acquisition due diligence and valuation risk,
|
|
plus a separate representations and warranties claim. Please provide the filing or URL,
|
|
then review and confirm the candidates before any apply step or database write goes live.
|
|
"""
|
|
|
|
assert all(reply_outcomes(reply).values())
|
|
|
|
|
|
def test_reply_outcomes_rejects_assertion_without_decomposition_or_iteration() -> None:
|
|
reply = "The claim is weak. I propose a better claim."
|
|
|
|
outcomes = reply_outcomes(reply)
|
|
|
|
assert outcomes["challenges_weak_support"] is False
|
|
assert outcomes["decomposes_the_bundled_legal_claim"] is False
|
|
assert outcomes["asks_for_user_iteration_before_live"] is False
|
|
|
|
|
|
def test_reply_outcomes_rejects_forbidden_stakeholder_alias() -> None:
|
|
otherwise_valid = """
|
|
The evidence is thin. I propose candidate claims about acquisition due diligence and
|
|
representations and warranties. Please review the source URL before any apply step goes live.
|
|
Next Cory-style follow-up: choose the claim framing.
|
|
"""
|
|
|
|
assert reply_outcomes(otherwise_valid)["uses_only_m3taversal_handle"] is False
|