"""Tests for the read-only workflow policy/guardrail visibility view (#646). Covers issue #646 acceptance criteria: 1. Console lists major guardrails with source pointers. 2. Secrets redacted. 3. Tests ensure sample secrets never appear. 4. Docs explain read-only nature (asserted here for the page copy; the doc itself is covered by inspection). """ import json import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from starlette.testclient import TestClient from webui import console_redaction from webui import policy_inventory from webui.app import create_app from webui.policy_inventory import ( PolicyEntry, PolicyInventorySnapshot, SourcePointer, load_policy_inventory, snapshot_to_dict, ) from webui.policy_views import render_policy_page def _entry(key, category, *, active=None, error=None): return PolicyEntry( key=key, title=key.replace("_", " ").title(), category=category, summary=f"summary for {key}", sources=(SourcePointer("src", f"{key}.py", "module"),), active=active, documented_default=None, diff=None, error=error, ) def _snapshot(entries): return PolicyInventorySnapshot( schema_version=1, read_only=True, note="read-only projection", entries=tuple(entries), categories=tuple(dict.fromkeys(e.category for e in entries)), build_errors=(), ) # The guardrail categories issue #646 names as in-scope. _EXPECTED_CATEGORIES = { "role_separation", "lease_rules", "worktree_rules", "merge_confirmation", "redaction", "contamination", "allocator_policy", "audit_logging", "mutation_gating", } class TestPolicyInventoryModel(unittest.TestCase): def test_major_guardrails_present(self): snapshot = load_policy_inventory() categories = {e.category for e in snapshot.entries} self.assertEqual(_EXPECTED_CATEGORIES, categories) self.assertGreaterEqual(len(snapshot.entries), len(_EXPECTED_CATEGORIES)) def test_every_guardrail_has_source_pointers(self): # AC1: source attribution (file/module/doc) for every guardrail. snapshot = load_policy_inventory() for entry in snapshot.entries: with self.subTest(entry=entry.key): self.assertTrue(entry.sources, "guardrail must carry source pointers") for source in entry.sources: self.assertTrue(source.path) self.assertIn(source.kind, {"module", "doc", "script", "config"}) def test_diff_reported_where_documented_default_declared(self): snapshot = load_policy_inventory() checked_any = False for entry in snapshot.entries: if entry.documented_default is None: self.assertIsNone(entry.diff) continue checked_any = True self.assertIsNotNone(entry.diff) self.assertEqual( entry.diff["status"], "matches_documented_default", f"{entry.key} drifted from its documented default: {entry.diff}", ) self.assertTrue(checked_any, "at least one guardrail should declare a default") def test_live_projections_populate_active(self): snapshot = load_policy_inventory() by_key = {e.key: e for e in snapshot.entries} for key in ("role_separation", "redaction", "audit_logging"): self.assertIsNone(by_key[key].error, f"{key} projection failed") self.assertIsInstance(by_key[key].active, dict) def test_build_entry_is_fail_soft_on_projection_error(self): def _boom(): raise RuntimeError("projection exploded") row = ( "redaction", "Secret redaction", "redaction", "summary", (SourcePointer("x", "webui/console_redaction.py", "module"),), _boom, {"redact_before_persist": True}, ) entry = policy_inventory._build_entry(row) self.assertIsNone(entry.active) self.assertIsNotNone(entry.error) self.assertEqual(entry.diff["status"], "active_unavailable") class TestPolicyRedaction(unittest.TestCase): def test_real_snapshot_has_no_secret_shapes(self): # AC3: the real emitted payload never carries a known secret shape. payload = snapshot_to_dict(load_policy_inventory()) self.assertEqual(console_redaction.scan_for_secrets(payload), []) def test_planted_keychain_secret_is_redacted(self): # AC2/AC3: a secret planted in an active projection is masked before emit. snapshot = _snapshot([ _entry( "redaction", "redaction", active={"leaked": "keychain:prgs-author-super-secret", "roles": ["author"]}, ) ]) payload = snapshot_to_dict(snapshot) blob = json.dumps(payload) self.assertNotIn("keychain:prgs-author-super-secret", blob) self.assertEqual(console_redaction.scan_for_secrets(payload), []) def test_planted_credential_assignment_is_redacted(self): snapshot = _snapshot([ _entry( "audit_logging", "audit_logging", active={"leaked": "token=abcd1234efgh5678", "append_only": True}, ) ]) payload = snapshot_to_dict(snapshot) blob = json.dumps(payload) self.assertNotIn("abcd1234efgh5678", blob) self.assertEqual(console_redaction.scan_for_secrets(payload), []) class TestPolicyRoutes(unittest.TestCase): def setUp(self): self.client = TestClient(create_app()) def test_policy_html_lists_guardrails_with_sources(self): response = self.client.get("/policy") self.assertEqual(response.status_code, 200) text = response.text self.assertIn("Workflow policy", text) self.assertIn("Role separation and RBAC", text) self.assertIn("Source pointers", text) self.assertIn("task_capability_map.py", text) self.assertIn("docs/safety-model.md", text) def test_policy_html_states_read_only(self): # AC4: the page explains its read-only nature. text = self.client.get("/policy").text self.assertIn("read-only", text.lower()) self.assertNotIn("