Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/LEARNING_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,59 @@ This file should be updated by Codex after each meaningful change.
### What to learn next
```

## 2026-05-26 - Connector management epic readiness

### What changed

Added an epic-level regression test for the protocol connection management and
operator console workstream.

### Why it matters

Issue #206 was implemented through a series of smaller schema, API, adapter,
Workbench, diagnostics, tag/source browser, test, and safety-doc issues. The
new regression test gives the parent epic one compact guard that checks those
surfaces still exist together.

### How it works

The test validates the OPC-UA, MQTT, and BACnet profile fixtures with the shared
contract, checks browser redaction of secret and certificate references, verifies
read-only structured test-connection errors, confirms the operator console
contains Sentinel and protocol-operation surfaces, checks read-only adapters
emit normalized `FactoryEvent` measurements, and asserts the safety docs reject
writeback and production-readiness claims.

### How to run it

```bash
.venv/bin/python -m pytest services/simulator/tests/test_connection_management_epic_readiness.py
```

### How to test it

```bash
make test
make lint
make typecheck
cd apps/web && npm test
cd apps/web && npm run lint
cd apps/web && npm run typecheck
make docs
git diff --check
```

### Key files

- `services/simulator/tests/test_connection_management_epic_readiness.py`
- `docs/LEARNING_LOG.md`

### What to learn next

Use this test as a parent-epic smoke guard when future connector work changes
profile contracts, protocol diagnostics, adapter source emission, or operator
console navigation.

## 2026-05-26 - Connector safety documentation

### What changed
Expand Down
141 changes: 141 additions & 0 deletions services/simulator/tests/test_connection_management_epic_readiness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import json
from pathlib import Path
from typing import Any

from factory_api.connection_profiles import serialize_connection_profile_for_browser
from factory_api.connection_tests import (
test_connection_profile_data as _test_connection_profile_data,
)
from factory_events import validate_connection_profile

REPO_ROOT = Path(__file__).resolve().parents[3]
PROFILE_FIXTURES = REPO_ROOT / "packages" / "test-fixtures" / "valid-connection-profiles"


def _content(path: str) -> str:
return (REPO_ROOT / path).read_text(encoding="utf-8")


def _json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))


def _raw_reference_values(value: Any) -> set[str]:
if isinstance(value, list):
return set().union(*(_raw_reference_values(item) for item in value))
if isinstance(value, dict):
refs = {value["ref"]} if isinstance(value.get("ref"), str) else set()
return refs.union(*(_raw_reference_values(item) for item in value.values()))
return set()


def test_issue_206_profile_contract_covers_protocols_and_redacts_browser_refs() -> None:
profiles = [
validate_connection_profile(_json(path))
for path in sorted(PROFILE_FIXTURES.glob("*_connection_profile.json"))
]

assert {profile.protocol for profile in profiles} == {"opcua", "mqtt", "bacnet"}

for profile in profiles:
raw_profile = profile.model_dump(mode="json")
browser_profile = serialize_connection_profile_for_browser(profile)
browser_payload = json.dumps(browser_profile, sort_keys=True)

for raw_ref in _raw_reference_values(raw_profile):
assert raw_ref not in browser_payload
if _raw_reference_values(raw_profile):
assert '"configured": true' in browser_payload
assert '"purpose":' in browser_payload


def test_issue_206_test_connection_results_are_read_only_structured_and_redacted() -> None:
profile = _json(PROFILE_FIXTURES / "opcua_connection_profile.json")
profile["endpoint"] = "opc.tcp://operator:secret-password@unreachable.local:4840/ofi"

result = _test_connection_profile_data(profile).model_dump(mode="json")

assert result["connection_id"] == "conn_opcua_filler_1"
assert result["protocol"] == "opcua"
assert result["status"] == "failed"
assert result["checked_at"]
assert result["duration_ms"] >= 0
assert result["diagnostics"] == {
"attempted": True,
"error_type": "ConnectionError",
"read_only": True,
}
assert "secret-password" not in json.dumps(result)
assert "opc.tcp://<redacted>@unreachable.local:4840/ofi" in result["message"]


def test_issue_206_operator_console_includes_protocol_and_sentinel_workflows() -> None:
layout = _content("apps/web/app/layout.tsx")
connections = _content("apps/web/app/connections/connections-workspace.tsx")
diagnostics = _content(
"apps/web/app/protocol-diagnostics/protocol-diagnostics-workspace.tsx"
)
tag_source = _content("apps/web/app/tag-source-browser/tag-source-browser-workspace.tsx")

for expected in [
"operator-sidebar",
"Workbench status strip",
"Sentinel workflows",
"Protocol operations",
'href: "/connections"',
'href: "/protocol-diagnostics"',
'href: "/tag-source-browser"',
"Read-only diagnostics",
"Writeback",
"Disabled",
]:
assert expected in layout

assert "OPC-UA controls" in connections
assert "MQTT controls" in connections
assert "BACnet controls" in connections
assert "raw passwords, tokens, private keys, or certificate" in connections
assert "Connection Diagnostics" in diagnostics
assert "Source Mapping Table" in diagnostics
assert "This page reads FIP API data only" in diagnostics
assert "Tag/Source Browser" in tag_source
assert "Grouped by:" in tag_source
assert "Source Details" in tag_source


def test_issue_206_read_only_adapters_emit_normalized_factory_events() -> None:
adapters = {
"bacnet": _content("services/ingestion/factory_ingestion/bacnet_read_only_adapter.py"),
"mqtt": _content("services/ingestion/factory_ingestion/mqtt_read_only_adapter.py"),
"opcua": _content("services/ingestion/factory_ingestion/opcua_read_only_adapter.py"),
}

for protocol, adapter_source in adapters.items():
assert f"run_{protocol}_read_only_adapter" in adapter_source
assert "EventEnvelope" in adapter_source
assert "ProcessMeasurementPayload" in adapter_source
assert "process.measurement.recorded" in adapter_source
assert f"{protocol}-read-only-adapter" in adapter_source
assert "events_store_path" in adapter_source


def test_issue_206_safety_docs_reject_writeback_and_production_readiness_claims() -> None:
safety_docs = "\n".join(
[
_content("docs/ARCHITECTURE.md"),
_content("docs/CONNECTOR_SAFETY_MODEL.md"),
_content("docs/SECURITY_MODEL.md"),
_content("docs/decisions/0002-production-protocol-connector-architecture.md"),
]
)

for expected in [
"read-only by default",
"No arbitrary tag writes",
"No product release, quarantine, hold, or other product disposition",
"No QMS/MES writeback",
"No production-readiness",
"must not expose raw credentials",
"Local Demo-Factory endpoints are development and test fixtures only",
]:
assert expected in safety_docs
Loading