Files
praxis/tests/test_guardrail_tuning.py
Praxis CI ec397f2c65 docs(milestone): complete v0.5-live-assist — v0.1.13 tagged, milestone release, merged to main
v0.5 (Live Assist — on-the-job voice companion) milestone complete.
4 phases: P0 (pre-execution, v0.1.10) → P1 (assist core + guardrail,
v0.1.11) → P2 (integration + tech-debt + NFR, v0.1.12) → P3 (final
review + ship, v0.1.13 = milestone release).

16/16 REQs covered (3 ASSIST + 4 NFR + 9 IDEATE). 4 v0.6 backlog.
469 tests passed, 0 failed. 1 P0 fixed (guardrail processor safety).
8 P1+ flagged for v0.6. 8 v0.4 P1+ tech-debt addressed.
G-049 + G-067 grill MUSTs resolved. ESCALATION-01 (PIPEDA) OPEN for
human legal review before assist surface go-live.

---ci---
project: praxis
phase: 3
milestone: v0.5
status: complete
requirements:
  covered: [REQ-ASSIST-01, REQ-ASSIST-02, REQ-ASSIST-03, REQ-NFR-ASSIST-01, REQ-NFR-ASSIST-02, REQ-NFR-ASSIST-03, REQ-NFR-ASSIST-04, REQ-IDEATE-01, REQ-IDEATE-02, REQ-IDEATE-03, REQ-IDEATE-04, REQ-IDEATE-05, REQ-IDEATE-06, REQ-IDEATE-07, REQ-IDEATE-08, REQ-IDEATE-09]
  partial: []
---/ci---
2026-08-04 22:35:56 +00:00

142 lines
6.4 KiB
Python

"""Guardrail tuning + adversarial bypass test (REQ-IDEATE-01, TASK-04-02, G-067).
Runs the LiveAssistGuardrail against the tuning corpus (tests/guardrail_corpus.py):
- Coaching responses: FP rate < 5% (REQ-IDEATE-04 target).
- Direct-answer responses: FN rate < 5% (the regex must catch these).
- False-authority: 100% blocked (hard violation).
- Adversarial: FN rate measured + reported (G-067 — ≤20% threshold for pilot,
documented acceptance; residual risk mitigated by defense-in-depth + v0.6
LLM-as-judge per REQ-IDEATE-10).
G-067 binding (GRILL-v0.5): the adversarial FN rate must be (a) measured pre-ship,
(b) compared against a threshold, (c) the threshold + rationale documented.
This test ASSERTS the measurement + the threshold; the threshold is ≤20% acceptable
for pilot because defense-in-depth (prompt + regex + audit) + the v0.6 LLM-as-judge
mitigate the residual risk. If the adversarial FN rate exceeds 20%, the test FAILS
(prompting a re-tuning wave or escalation per G-067).
"""
from __future__ import annotations
import asyncio
import logging
import pytest
from server.guardrails.live_assist import LiveAssistGuardrail
from server.services.base import GuardrailContext
from tests.guardrail_corpus import (
ADVERSARIAL_RESPONSES,
COACHING_RESPONSES,
DIRECT_ANSWER_RESPONSES,
FALSE_AUTHORITY_RESPONSES,
)
log = logging.getLogger(__name__)
# G-067 binding threshold: adversarial FN ≤ 20% acceptable for pilot.
ADVERSARIAL_FN_THRESHOLD = 0.20
# REQ-IDEATE-04 targets.
COACHING_FP_THRESHOLD = 0.05 # < 5%
DIRECT_FN_THRESHOLD = 0.05 # < 5%
def _run_check(text: str):
g = LiveAssistGuardrail()
return asyncio.run(g.check(text, GuardrailContext(role="assist")))
def _fp_rate(corpus, expected_allowed: bool) -> tuple[float, int, int]:
"""Compute the false-positive rate (allowed != expected_allowed)."""
misclassified = 0
total = 0
for entry in corpus:
v = _run_check(entry["text"])
total += 1
if v.allowed != expected_allowed:
misclassified += 1
return (misclassified / total if total else 0.0), misclassified, total
def test_coaching_responses_allowed():
"""All COACHING_RESPONSES → allowed=True. FP rate < 5% (REQ-IDEATE-04)."""
fp, mis, total = _fp_rate(COACHING_RESPONSES, expected_allowed=True)
log.info("coaching FP rate: %.1%% (%d/%d)", fp * 100, mis, total)
print(f"\n[guardrail-tuning] coaching FP rate: {fp:.1%} ({mis}/{total})")
assert fp < COACHING_FP_THRESHOLD, (
f"coaching FP rate {fp:.1%} exceeds {COACHING_FP_THRESHOLD:.0%}"
f"the regex is over-matching (tune it). {mis}/{total} blocked."
)
def test_direct_answer_responses_blocked():
"""All DIRECT_ANSWER_RESPONSES → allowed=False. FN rate < 5%."""
fn, mis, total = _fp_rate(DIRECT_ANSWER_RESPONSES, expected_allowed=False)
log.info("direct-answer FN rate: %.1%% (%d/%d)", fn * 100, mis, total)
print(f"\n[guardrail-tuning] direct-answer FN rate: {fn:.1%} ({mis}/{total})")
assert fn < DIRECT_FN_THRESHOLD, (
f"direct-answer FN rate {fn:.1%} exceeds {DIRECT_FN_THRESHOLD:.0%}"
f"the regex is under-matching (tune it). {mis}/{total} slipped through."
)
def test_false_authority_responses_blocked():
"""All FALSE_AUTHORITY_RESPONSES → allowed=False (100% — hard violation)."""
fn, mis, total = _fp_rate(FALSE_AUTHORITY_RESPONSES, expected_allowed=False)
log.info("false-authority FN rate: %.1%% (%d/%d)", fn * 100, mis, total)
print(f"\n[guardrail-tuning] false-authority FN rate: {fn:.1%} ({mis}/{total})")
assert fn == 0.0, (
f"false-authority FN rate {fn:.1%} must be 0% (hard violation). "
f"{mis}/{total} slipped through."
)
def test_adversarial_responses_g067():
"""G-067 binding: adversarial FN rate measured + compared against ≤20% threshold.
The adversarial corpus is paraphrased direct answers designed to slip past
the regex. The FN rate is the residual risk, mitigated by defense-in-depth
(prompt + regex + audit) + the v0.6 LLM-as-judge (REQ-IDEATE-10).
"""
fn, mis, total = _fp_rate(ADVERSARIAL_RESPONSES, expected_allowed=False)
log.info("adversarial FN rate: %.1%% (%d/%d)", fn * 100, mis, total)
print(
f"\n[guardrail-tuning] adversarial false-negative rate: {fn:.1%} "
f"({mis}/{total}) — defense-in-depth + post-v0.5 LLM-as-judge mitigates"
)
# G-067: the adversarial FN rate must be ≤ 20% for pilot acceptance.
assert fn <= ADVERSARIAL_FN_THRESHOLD, (
f"adversarial FN rate {fn:.1%} exceeds G-067 threshold "
f"{ADVERSARIAL_FN_THRESHOLD:.0%} — re-tune the regex or escalate. "
f"{mis}/{total} paraphrased direct answers slipped through."
)
def test_tuning_summary():
"""Print the full tuning summary (FP + FN + accuracy) — REQ-IDEATE-04 measurement."""
coaching_fp, c_mis, c_total = _fp_rate(COACHING_RESPONSES, expected_allowed=True)
direct_fn, d_mis, d_total = _fp_rate(DIRECT_ANSWER_RESPONSES, expected_allowed=False)
fa_fn, f_mis, f_total = _fp_rate(FALSE_AUTHORITY_RESPONSES, expected_allowed=False)
adv_fn, a_mis, a_total = _fp_rate(ADVERSARIAL_RESPONSES, expected_allowed=False)
# Overall accuracy across the full corpus (excluding adversarial — those
# are the residual-risk set, not the tuning target).
total_correct = (c_total - c_mis) + (d_total - d_mis) + (f_total - f_mis)
total_n = c_total + d_total + f_total
accuracy = total_correct / total_n if total_n else 0.0
print(
f"\n[guardrail-tuning] SUMMARY:\n"
f" coaching FP rate: {coaching_fp:.1%} ({c_mis}/{c_total}) — target <{COACHING_FP_THRESHOLD:.0%}\n"
f" direct-answer FN rate: {direct_fn:.1%} ({d_mis}/{d_total}) — target <{DIRECT_FN_THRESHOLD:.0%}\n"
f" false-authority FN: {fa_fn:.1%} ({f_mis}/{f_total}) — target 0%\n"
f" adversarial FN rate: {adv_fn:.1%} ({a_mis}/{a_total}) — G-067 threshold ≤{ADVERSARIAL_FN_THRESHOLD:.0%}\n"
f" overall accuracy: {accuracy:.1%} ({total_correct}/{total_n})"
)
# G-067 documentation: the threshold + rationale are documented in the
# assertion messages above + this test's docstring. The measurement is
# CI-visible (printed) for the verify stage.
assert coaching_fp < COACHING_FP_THRESHOLD
assert direct_fn < DIRECT_FN_THRESHOLD
assert fa_fn == 0.0
assert adv_fn <= ADVERSARIAL_FN_THRESHOLD