feat(sense): add reconciliation safety controls [T-012]
This commit is contained in:
@@ -56,6 +56,8 @@ class PostgresContractTests(unittest.TestCase):
|
||||
"007_privileges_area_audit.sql",
|
||||
"008_control_api.sql",
|
||||
"009_privileges_control_api.sql",
|
||||
"010_reconcile_safety.sql",
|
||||
"011_privileges_reconcile_safety.sql",
|
||||
],
|
||||
names,
|
||||
)
|
||||
@@ -109,7 +111,7 @@ class PostgresContractTests(unittest.TestCase):
|
||||
"initdb.exe",
|
||||
"pg_ctl.exe",
|
||||
"127.0.0.1",
|
||||
"yovision-t011-pg-",
|
||||
"yovision-t012-pg-",
|
||||
"YOVISION_TEST_POSTGRES_DSN",
|
||||
"Get-NetTCPConnection",
|
||||
):
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Static safety checks for the T-012 reconciliation/orphan boundary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def text(relative: str) -> str:
|
||||
return (ROOT / relative).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def normalized(value: str) -> str:
|
||||
return re.sub(r"\s+", " ", value.lower())
|
||||
|
||||
|
||||
class SenseReconcileSafetyTests(unittest.TestCase):
|
||||
def test_postgres_v5_has_fencing_ownership_and_private_reports(self) -> None:
|
||||
schema = normalized(text("deploy/postgres/010_reconcile_safety.sql"))
|
||||
privileges = normalized(
|
||||
text("deploy/postgres/011_privileges_reconcile_safety.sql")
|
||||
)
|
||||
implementation = normalized(text("Sense/internal/store/postgres.go"))
|
||||
for table in (
|
||||
"media_path_ownership",
|
||||
"operational_leases",
|
||||
"orphan_scan_runs",
|
||||
"orphan_scan_findings",
|
||||
"orphan_cleanup_actions",
|
||||
):
|
||||
self.assertIn(f"sense.{table}", schema)
|
||||
self.assertIn(f"sense.{table}", privileges)
|
||||
self.assertIn("insert into sense.schema_migrations(version) values (5)", schema)
|
||||
self.assertIn("clock_timestamp()", implementation)
|
||||
self.assertIn("for update of r skip locked", implementation)
|
||||
self.assertIn("and lease_token = $5", implementation)
|
||||
self.assertNotIn("endpoint_ref", schema)
|
||||
self.assertNotIn("credential_ref", schema)
|
||||
self.assertNotIn("source_uri", schema)
|
||||
|
||||
def test_cleanup_gate_has_no_force_or_unowned_delete_path(self) -> None:
|
||||
source = text("Sense/internal/orphan/orphan.go")
|
||||
normalized_source = normalized(source)
|
||||
self.assertIn("candidates*100 > observed*10", normalized_source)
|
||||
self.assertIn("candidates > maxcleanupitems", normalized_source)
|
||||
self.assertIn('confirmation != "delete "+scanid', normalized_source)
|
||||
self.assertIn(
|
||||
"finding.classification != store.orphanownedstale", normalized_source
|
||||
)
|
||||
self.assertNotRegex(normalized_source, r"\bforce\b|bypass")
|
||||
migration = normalized(text("deploy/postgres/010_reconcile_safety.sql"))
|
||||
self.assertIn("classification = 'owned_stale'", migration)
|
||||
|
||||
def test_mediamtx_inventory_discards_sources_and_bounds_pagination(self) -> None:
|
||||
source = text("Sense/internal/mtx/client.go")
|
||||
self.assertIn("ListPathNames", source)
|
||||
self.assertIn("maxPages", source)
|
||||
self.assertIn("list paths repeated page", source)
|
||||
inventory = source[source.index("func (c *Client) ListPathNames") :]
|
||||
inventory = inventory[: inventory.index("func (c *Client) CreatePath")]
|
||||
self.assertNotIn("item.Source", inventory)
|
||||
|
||||
def test_metrics_are_fixed_and_control_contract_is_untouched(self) -> None:
|
||||
metrics = text("Sense/internal/metrics/metrics.go")
|
||||
main = text("Sense/cmd/sense-api/main.go")
|
||||
self.assertIn('mux.Handle("GET /metrics", registry.Handler())', main)
|
||||
for forbidden in ("tenant_id", "site_id", "device_id", "path_name"):
|
||||
self.assertNotIn(forbidden, metrics)
|
||||
control = text("docs/contracts/sense-control-v1.openapi.json")
|
||||
self.assertEqual(7, len(re.findall(r'"operationId"\s*:', control)))
|
||||
|
||||
def test_operator_command_requires_fresh_scan_confirmation(self) -> None:
|
||||
command = text("Sense/cmd/sense-orphan/main.go")
|
||||
manager = text("Sense/internal/orphan/orphan.go")
|
||||
self.assertIn("exact confirmation: DELETE <scan-id>", command)
|
||||
self.assertIn("scan.ExpiresAt.After(now)", manager)
|
||||
self.assertIn("cleanupCandidates(scan, paths, ownership)", manager)
|
||||
self.assertIn("RecordOrphanCleanup", manager)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user