Skip to content

Commit 29802bf

Browse files
authored
Merge pull request #245 from Open-Factory-Initiative/test/issue-206-connector-epic-readiness
Test: Add connector management epic readiness guard
2 parents 1d5aaac + cd8d123 commit 29802bf

2 files changed

Lines changed: 194 additions & 0 deletions

File tree

docs/LEARNING_LOG.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,59 @@ This file should be updated by Codex after each meaningful change.
2222
### What to learn next
2323
```
2424

25+
## 2026-05-26 - Connector management epic readiness
26+
27+
### What changed
28+
29+
Added an epic-level regression test for the protocol connection management and
30+
operator console workstream.
31+
32+
### Why it matters
33+
34+
Issue #206 was implemented through a series of smaller schema, API, adapter,
35+
Workbench, diagnostics, tag/source browser, test, and safety-doc issues. The
36+
new regression test gives the parent epic one compact guard that checks those
37+
surfaces still exist together.
38+
39+
### How it works
40+
41+
The test validates the OPC-UA, MQTT, and BACnet profile fixtures with the shared
42+
contract, checks browser redaction of secret and certificate references, verifies
43+
read-only structured test-connection errors, confirms the operator console
44+
contains Sentinel and protocol-operation surfaces, checks read-only adapters
45+
emit normalized `FactoryEvent` measurements, and asserts the safety docs reject
46+
writeback and production-readiness claims.
47+
48+
### How to run it
49+
50+
```bash
51+
.venv/bin/python -m pytest services/simulator/tests/test_connection_management_epic_readiness.py
52+
```
53+
54+
### How to test it
55+
56+
```bash
57+
make test
58+
make lint
59+
make typecheck
60+
cd apps/web && npm test
61+
cd apps/web && npm run lint
62+
cd apps/web && npm run typecheck
63+
make docs
64+
git diff --check
65+
```
66+
67+
### Key files
68+
69+
- `services/simulator/tests/test_connection_management_epic_readiness.py`
70+
- `docs/LEARNING_LOG.md`
71+
72+
### What to learn next
73+
74+
Use this test as a parent-epic smoke guard when future connector work changes
75+
profile contracts, protocol diagnostics, adapter source emission, or operator
76+
console navigation.
77+
2578
## 2026-05-26 - Connector safety documentation
2679

2780
### What changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import json
2+
from pathlib import Path
3+
from typing import Any
4+
5+
from factory_api.connection_profiles import serialize_connection_profile_for_browser
6+
from factory_api.connection_tests import (
7+
test_connection_profile_data as _test_connection_profile_data,
8+
)
9+
from factory_events import validate_connection_profile
10+
11+
REPO_ROOT = Path(__file__).resolve().parents[3]
12+
PROFILE_FIXTURES = REPO_ROOT / "packages" / "test-fixtures" / "valid-connection-profiles"
13+
14+
15+
def _content(path: str) -> str:
16+
return (REPO_ROOT / path).read_text(encoding="utf-8")
17+
18+
19+
def _json(path: Path) -> dict[str, Any]:
20+
return json.loads(path.read_text(encoding="utf-8"))
21+
22+
23+
def _raw_reference_values(value: Any) -> set[str]:
24+
if isinstance(value, list):
25+
return set().union(*(_raw_reference_values(item) for item in value))
26+
if isinstance(value, dict):
27+
refs = {value["ref"]} if isinstance(value.get("ref"), str) else set()
28+
return refs.union(*(_raw_reference_values(item) for item in value.values()))
29+
return set()
30+
31+
32+
def test_issue_206_profile_contract_covers_protocols_and_redacts_browser_refs() -> None:
33+
profiles = [
34+
validate_connection_profile(_json(path))
35+
for path in sorted(PROFILE_FIXTURES.glob("*_connection_profile.json"))
36+
]
37+
38+
assert {profile.protocol for profile in profiles} == {"opcua", "mqtt", "bacnet"}
39+
40+
for profile in profiles:
41+
raw_profile = profile.model_dump(mode="json")
42+
browser_profile = serialize_connection_profile_for_browser(profile)
43+
browser_payload = json.dumps(browser_profile, sort_keys=True)
44+
45+
for raw_ref in _raw_reference_values(raw_profile):
46+
assert raw_ref not in browser_payload
47+
if _raw_reference_values(raw_profile):
48+
assert '"configured": true' in browser_payload
49+
assert '"purpose":' in browser_payload
50+
51+
52+
def test_issue_206_test_connection_results_are_read_only_structured_and_redacted() -> None:
53+
profile = _json(PROFILE_FIXTURES / "opcua_connection_profile.json")
54+
profile["endpoint"] = "opc.tcp://operator:secret-password@unreachable.local:4840/ofi"
55+
56+
result = _test_connection_profile_data(profile).model_dump(mode="json")
57+
58+
assert result["connection_id"] == "conn_opcua_filler_1"
59+
assert result["protocol"] == "opcua"
60+
assert result["status"] == "failed"
61+
assert result["checked_at"]
62+
assert result["duration_ms"] >= 0
63+
assert result["diagnostics"] == {
64+
"attempted": True,
65+
"error_type": "ConnectionError",
66+
"read_only": True,
67+
}
68+
assert "secret-password" not in json.dumps(result)
69+
assert "opc.tcp://<redacted>@unreachable.local:4840/ofi" in result["message"]
70+
71+
72+
def test_issue_206_operator_console_includes_protocol_and_sentinel_workflows() -> None:
73+
layout = _content("apps/web/app/layout.tsx")
74+
connections = _content("apps/web/app/connections/connections-workspace.tsx")
75+
diagnostics = _content(
76+
"apps/web/app/protocol-diagnostics/protocol-diagnostics-workspace.tsx"
77+
)
78+
tag_source = _content("apps/web/app/tag-source-browser/tag-source-browser-workspace.tsx")
79+
80+
for expected in [
81+
"operator-sidebar",
82+
"Workbench status strip",
83+
"Sentinel workflows",
84+
"Protocol operations",
85+
'href: "/connections"',
86+
'href: "/protocol-diagnostics"',
87+
'href: "/tag-source-browser"',
88+
"Read-only diagnostics",
89+
"Writeback",
90+
"Disabled",
91+
]:
92+
assert expected in layout
93+
94+
assert "OPC-UA controls" in connections
95+
assert "MQTT controls" in connections
96+
assert "BACnet controls" in connections
97+
assert "raw passwords, tokens, private keys, or certificate" in connections
98+
assert "Connection Diagnostics" in diagnostics
99+
assert "Source Mapping Table" in diagnostics
100+
assert "This page reads FIP API data only" in diagnostics
101+
assert "Tag/Source Browser" in tag_source
102+
assert "Grouped by:" in tag_source
103+
assert "Source Details" in tag_source
104+
105+
106+
def test_issue_206_read_only_adapters_emit_normalized_factory_events() -> None:
107+
adapters = {
108+
"bacnet": _content("services/ingestion/factory_ingestion/bacnet_read_only_adapter.py"),
109+
"mqtt": _content("services/ingestion/factory_ingestion/mqtt_read_only_adapter.py"),
110+
"opcua": _content("services/ingestion/factory_ingestion/opcua_read_only_adapter.py"),
111+
}
112+
113+
for protocol, adapter_source in adapters.items():
114+
assert f"run_{protocol}_read_only_adapter" in adapter_source
115+
assert "EventEnvelope" in adapter_source
116+
assert "ProcessMeasurementPayload" in adapter_source
117+
assert "process.measurement.recorded" in adapter_source
118+
assert f"{protocol}-read-only-adapter" in adapter_source
119+
assert "events_store_path" in adapter_source
120+
121+
122+
def test_issue_206_safety_docs_reject_writeback_and_production_readiness_claims() -> None:
123+
safety_docs = "\n".join(
124+
[
125+
_content("docs/ARCHITECTURE.md"),
126+
_content("docs/CONNECTOR_SAFETY_MODEL.md"),
127+
_content("docs/SECURITY_MODEL.md"),
128+
_content("docs/decisions/0002-production-protocol-connector-architecture.md"),
129+
]
130+
)
131+
132+
for expected in [
133+
"read-only by default",
134+
"No arbitrary tag writes",
135+
"No product release, quarantine, hold, or other product disposition",
136+
"No QMS/MES writeback",
137+
"No production-readiness",
138+
"must not expose raw credentials",
139+
"Local Demo-Factory endpoints are development and test fixtures only",
140+
]:
141+
assert expected in safety_docs

0 commit comments

Comments
 (0)