Skip to content

Commit 05ebb1d

Browse files
Gabriel JangGabriel Jang
authored andcommitted
fix: verify isolated docker integration and refine system health replay
1 parent 6d02cdf commit 05ebb1d

8 files changed

Lines changed: 486 additions & 34 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,14 @@ Local setup:
148148
cp infra/.env.example infra/.env
149149
chmod 0600 infra/.env
150150
# Replace placeholder values in infra/.env without committing the file.
151+
# If 8000/5173 are already occupied, set FABOPS_API_PORT/FABOPS_WEB_PORT to free loopback ports.
151152

152153
docker compose --env-file infra/.env -f infra/docker-compose.yml config --quiet
153-
docker compose --env-file infra/.env -f infra/docker-compose.yml up -d --build
154-
curl -fsS http://127.0.0.1:8000/health/ready
155-
docker compose --env-file infra/.env -f infra/docker-compose.yml down
154+
uv run python -m evaluation.m6_integration --output /tmp/fabops-m6-container-integration.json --check
156155
```
157156

157+
The M6 integration verifier reads only the non-secret `FABOPS_API_PORT` setting needed for its loopback readiness probe and defaults to `8000` for backward compatibility. It never copies `infra/.env` contents into evidence. Canonical verification also injects isolated free `FABOPS_E2E_API_PORT` / `FABOPS_E2E_WEB_PORT` values into Playwright so unrelated listeners on the normal development ports are not reused or stopped. Direct `npm run test:e2e` behavior remains unchanged and still defaults to `8000/5173` unless those E2E variables are provided.
158+
158159
## Canonical verification
159160

160161
The project has one release verification entry point:

‎evaluation/canonical_verify.py‎

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import platform
77
import shlex
88
import shutil
9+
import socket
910
import subprocess
1011
import tarfile
1112
import tempfile
@@ -40,17 +41,21 @@ def _run(
4041
*,
4142
cwd: Path = ROOT,
4243
extra_paths: list[Path] | None = None,
44+
env_overrides: dict[str, str] | None = None,
4345
timeout: int = 900,
4446
) -> dict[str, Any]:
4547
started = time.perf_counter()
48+
environment = os.environ.copy()
49+
if env_overrides:
50+
environment.update(env_overrides)
4651
completed = subprocess.run(
4752
arguments,
4853
cwd=cwd,
4954
capture_output=True,
5055
text=True,
5156
timeout=timeout,
5257
check=False,
53-
env=os.environ.copy(),
58+
env=environment,
5459
)
5560
duration = time.perf_counter() - started
5661
output = "\n".join([completed.stdout, completed.stderr]).strip()
@@ -66,6 +71,48 @@ def _run(
6671
}
6772

6873

74+
def _allocate_loopback_port(excluded: set[int]) -> int:
75+
for _ in range(32):
76+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
77+
probe.bind(("127.0.0.1", 0))
78+
port = int(probe.getsockname()[1])
79+
if port not in excluded:
80+
return port
81+
raise RuntimeError("unable to allocate an isolated loopback port")
82+
83+
84+
def _select_isolated_e2e_ports() -> dict[str, int]:
85+
excluded = {8000, 5173}
86+
api_port = _allocate_loopback_port(excluded)
87+
excluded.add(api_port)
88+
web_port = _allocate_loopback_port(excluded)
89+
return {"api_port": api_port, "web_port": web_port}
90+
91+
92+
def _run_frontend_e2e(npm: str) -> tuple[dict[str, Any], dict[str, int]]:
93+
ports: dict[str, int] = {}
94+
step: dict[str, Any] = {}
95+
for attempt in range(3):
96+
ports = _select_isolated_e2e_ports()
97+
step = _run(
98+
"frontend-e2e",
99+
[npm, "run", "test:e2e"],
100+
cwd=ROOT / "systems/web",
101+
env_overrides={
102+
"FABOPS_E2E_API_PORT": str(ports["api_port"]),
103+
"FABOPS_E2E_WEB_PORT": str(ports["web_port"]),
104+
},
105+
timeout=300,
106+
)
107+
if step["exit_code"] == 0:
108+
return step, ports
109+
output = step.get("output_tail", "").lower()
110+
port_conflict = "address already in use" in output or "eaddrinuse" in output
111+
if not port_conflict or attempt == 2:
112+
return step, ports
113+
return step, ports
114+
115+
69116
def _clean_setup(uv: str, npm: str, temp_root: Path) -> dict[str, Any]:
70117
archive = temp_root / "source.tar"
71118
snapshot = temp_root / "snapshot"
@@ -133,14 +180,19 @@ def verify(output: Path) -> dict[str, Any]:
133180
_run("frontend-component-tests", [npm, "run", "test"], cwd=ROOT / "systems/web"),
134181
_run("frontend-build", [npm, "run", "build"], cwd=ROOT / "systems/web"),
135182
_run("frontend-audit", [npm, "audit", "--audit-level=high"], cwd=ROOT / "systems/web"),
136-
_run("frontend-e2e", [npm, "run", "test:e2e"], cwd=ROOT / "systems/web", timeout=300),
137-
_run(
138-
"architecture-fitness",
139-
[uv, "run", "python", "-m", "evaluation.m6_fitness", "--output-dir", str(temp_fitness)],
140-
extra_paths=[temp_dir],
141-
),
142-
_run("release-manifest-consistency", [uv, "run", "python", "-m", "evaluation.release_manifest", "--check"]),
143183
]
184+
e2e_step, e2e_ports = _run_frontend_e2e(npm)
185+
steps.extend(
186+
[
187+
e2e_step,
188+
_run(
189+
"architecture-fitness",
190+
[uv, "run", "python", "-m", "evaluation.m6_fitness", "--output-dir", str(temp_fitness)],
191+
extra_paths=[temp_dir],
192+
),
193+
_run("release-manifest-consistency", [uv, "run", "python", "-m", "evaluation.release_manifest", "--check"]),
194+
]
195+
)
144196

145197
docker_info = _run("docker-info", [docker, "info"], timeout=30)
146198
docker_available = docker_info["exit_code"] == 0
@@ -197,6 +249,7 @@ def verify(output: Path) -> dict[str, Any]:
197249
"expected_evaluation_hash": expected_evaluation_hash,
198250
"evaluation_identity_passed": evaluation_identity_passed,
199251
"steps": steps,
252+
"e2e_ports": e2e_ports,
200253
"docker_integration": docker_status,
201254
"clean_setup": clean_setup,
202255
"policy": {

‎evaluation/m6_integration.py‎

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,33 @@
1212

1313
INTEGRATION_VERSION = "m6-container-integration-v1"
1414
COMPOSE = ["docker", "compose", "--env-file", "infra/.env", "-f", "infra/docker-compose.yml"]
15+
ENV_FILE = Path("infra/.env")
16+
DEFAULT_API_PORT = 8000
17+
18+
19+
def _validate_port(raw_value: str, *, source: str) -> int:
20+
try:
21+
port = int(raw_value)
22+
except ValueError as exc:
23+
raise ValueError(f"{source} must be an integer in range 1-65535") from exc
24+
if not 1 <= port <= 65535:
25+
raise ValueError(f"{source} must be an integer in range 1-65535")
26+
return port
27+
28+
29+
def resolve_api_port(env_file: Path = ENV_FILE, *, explicit_port: str | None = None) -> int:
30+
if explicit_port is not None:
31+
return _validate_port(explicit_port, source="--api-port")
32+
if not env_file.exists():
33+
return DEFAULT_API_PORT
34+
for raw_line in env_file.read_text(encoding="utf-8").splitlines():
35+
line = raw_line.strip()
36+
if not line or line.startswith("#") or "=" not in line:
37+
continue
38+
key, value = line.split("=", 1)
39+
if key.strip() == "FABOPS_API_PORT":
40+
return _validate_port(value.strip(), source="FABOPS_API_PORT")
41+
return DEFAULT_API_PORT
1542

1643

1744
def _run(arguments: list[str], *, timeout: int = 300) -> dict[str, Any]:
@@ -27,18 +54,19 @@ def _run(arguments: list[str], *, timeout: int = 300) -> dict[str, Any]:
2754
}
2855

2956

30-
def _read_health(timeout_seconds: float = 60.0) -> dict[str, Any] | None:
57+
def _read_health(host: str, port: int, timeout_seconds: float = 60.0) -> dict[str, Any] | None:
3158
deadline = time.monotonic() + timeout_seconds
59+
health_url = f"http://{host}:{port}/health/ready"
3260
while time.monotonic() < deadline:
3361
try:
34-
with urllib.request.urlopen("http://127.0.0.1:8000/health/ready", timeout=3) as response:
62+
with urllib.request.urlopen(health_url, timeout=3) as response:
3563
return json.load(response)
3664
except (OSError, urllib.error.URLError, json.JSONDecodeError):
3765
time.sleep(1.0)
3866
return None
3967

4068

41-
def generate(output: Path, *, keep_up: bool = False) -> dict[str, Any]:
69+
def generate(output: Path, *, keep_up: bool = False, api_port_override: str | None = None) -> dict[str, Any]:
4270
generated_at = datetime.now(timezone.utc).isoformat()
4371
commands: list[dict[str, Any]] = []
4472
docker_info = _run(["docker", "info"], timeout=30)
@@ -68,6 +96,55 @@ def generate(output: Path, *, keep_up: bool = False) -> dict[str, Any]:
6896
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
6997
return result
7098

99+
if not ENV_FILE.exists():
100+
result = {
101+
"schema_version": INTEGRATION_VERSION,
102+
"generated_at": generated_at,
103+
"status": "unverified",
104+
"reason": "Local server-only infra/.env unavailable; container-backed integration was not executed",
105+
"compose_config_verified": False,
106+
"postgres_runtime_verified": False,
107+
"redpanda_runtime_verified": False,
108+
"neo4j_runtime_verified": False,
109+
"container_integration_verified": False,
110+
"api_restart_verified": False,
111+
"docker_daemon_available": True,
112+
"api_port": DEFAULT_API_PORT,
113+
"commands": [{"command": "docker info", "exit_code": docker_info["exit_code"]}],
114+
"reproduction_commands": [
115+
"cp infra/.env.example infra/.env",
116+
"chmod 0600 infra/.env",
117+
"docker compose --env-file infra/.env -f infra/docker-compose.yml config --quiet",
118+
],
119+
}
120+
output.parent.mkdir(parents=True, exist_ok=True)
121+
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
122+
return result
123+
124+
try:
125+
api_port = resolve_api_port(ENV_FILE, explicit_port=api_port_override)
126+
except ValueError:
127+
result = {
128+
"schema_version": INTEGRATION_VERSION,
129+
"generated_at": generated_at,
130+
"status": "unverified",
131+
"reason": "Invalid integration API port configuration; expected an integer in range 1-65535",
132+
"compose_config_verified": False,
133+
"postgres_runtime_verified": False,
134+
"redpanda_runtime_verified": False,
135+
"neo4j_runtime_verified": False,
136+
"container_integration_verified": False,
137+
"api_restart_verified": False,
138+
"docker_daemon_available": True,
139+
"commands": [{"command": "docker info", "exit_code": docker_info["exit_code"]}],
140+
"reproduction_commands": [
141+
"docker compose --env-file infra/.env -f infra/docker-compose.yml config --quiet",
142+
],
143+
}
144+
output.parent.mkdir(parents=True, exist_ok=True)
145+
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
146+
return result
147+
71148
compose_config = _run([*COMPOSE, "config", "--quiet"], timeout=30)
72149
commands.append({key: value for key, value in compose_config.items() if key not in {"stdout_tail", "stderr_tail"}})
73150
compose_config_verified = compose_config["exit_code"] == 0
@@ -84,6 +161,7 @@ def generate(output: Path, *, keep_up: bool = False) -> dict[str, Any]:
84161
"container_integration_verified": False,
85162
"api_restart_verified": False,
86163
"docker_daemon_available": True,
164+
"api_port": api_port,
87165
"commands": commands,
88166
"reproduction_commands": [compose_config["command"]],
89167
}
@@ -99,7 +177,7 @@ def generate(output: Path, *, keep_up: bool = False) -> dict[str, Any]:
99177
restart: dict[str, Any] = {"exit_code": 1, "command": "not executed", "duration_seconds": 0.0}
100178
try:
101179
if up["exit_code"] == 0:
102-
health = _read_health()
180+
health = _read_health("127.0.0.1", api_port)
103181
integration_test = _run(
104182
[
105183
*COMPOSE,
@@ -120,7 +198,7 @@ def generate(output: Path, *, keep_up: bool = False) -> dict[str, Any]:
120198
restart = _run([*COMPOSE, "restart", "api"], timeout=60)
121199
commands.append({key: value for key, value in restart.items() if key not in {"stdout_tail", "stderr_tail"}})
122200
if restart["exit_code"] == 0:
123-
restart_health = _read_health()
201+
restart_health = _read_health("127.0.0.1", api_port)
124202
finally:
125203
if not keep_up:
126204
down = _run([*COMPOSE, "down"], timeout=120)
@@ -149,6 +227,7 @@ def generate(output: Path, *, keep_up: bool = False) -> dict[str, Any]:
149227
"status": "verified" if container_verified else "degraded",
150228
"reason": None if container_verified else "one or more container-backed runtime checks failed",
151229
"docker_daemon_available": True,
230+
"api_port": api_port,
152231
"compose_config_verified": compose_config_verified,
153232
"postgres_runtime_verified": postgres_verified,
154233
"redpanda_runtime_verified": redpanda_verified,
@@ -181,9 +260,10 @@ def main() -> None:
181260
parser = argparse.ArgumentParser(description="Verify the isolated M6 Docker Compose integration stack.")
182261
parser.add_argument("--output", type=Path, default=Path("evidence/m6/integration-summary.json"))
183262
parser.add_argument("--keep-up", action="store_true")
263+
parser.add_argument("--api-port", help="Override the loopback API port used for readiness probes.")
184264
parser.add_argument("--check", action="store_true")
185265
args = parser.parse_args()
186-
result = generate(args.output, keep_up=args.keep_up)
266+
result = generate(args.output, keep_up=args.keep_up, api_port_override=args.api_port)
187267
if args.check and result["docker_daemon_available"] and not result["container_integration_verified"]:
188268
raise SystemExit("M6 container integration verification failed")
189269

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"schema_version": "m6-container-integration-evidence-v1",
3+
"generated_at": "2026-08-23T23:34:51.108261+00:00",
4+
"verification_source_git_sha": "6d02cdf628cfd61d685f078b8a2c15b6857c0bcf",
5+
"platform": {
6+
"machine": "arm64",
7+
"runtime": "Docker Desktop",
8+
"docker_architecture": "aarch64"
9+
},
10+
"loopback_ports": {
11+
"api": 18090,
12+
"web": 15190,
13+
"conflict_avoidance": "Existing unrelated default-port listener was preserved; isolated FabOps loopback ports were used instead."
14+
},
15+
"status": "verified",
16+
"compose_project": "fabops-decision-lab-m6",
17+
"compose_config_verified": true,
18+
"postgres_runtime_verified": true,
19+
"redpanda_runtime_verified": true,
20+
"neo4j_runtime_verified": true,
21+
"neo4j_memory_limit": "768m",
22+
"container_integration_verified": true,
23+
"container_pytest": {
24+
"exit_code": 0,
25+
"result": "4 passed"
26+
},
27+
"api_restart_verified": true,
28+
"actual_equipment_control": false,
29+
"duration_seconds": 79.253,
30+
"cleanup": {
31+
"compose_down_completed": true,
32+
"volumes_removed": false
33+
},
34+
"reproduction_commands": [
35+
"docker compose --env-file infra/.env -f infra/docker-compose.yml config --quiet",
36+
"uv run python -m evaluation.m6_integration --output /tmp/fabops-m6-container-integration-final.json --check",
37+
"docker compose --env-file infra/.env -f infra/docker-compose.yml down"
38+
],
39+
"sanitization": {
40+
"env_contents_included": false,
41+
"credentials_included": false,
42+
"credential_bearing_dsns_included": false
43+
}
44+
}

‎systems/web/src/App.test.tsx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,20 @@ describe("FabOps workbench", () => {
155155
expect(await screen.findByRole("heading", {name: "Event-backed decision trace"})).toBeInTheDocument();
156156
expect(screen.getByLabelText("Replay scrubber")).toHaveValue("0");
157157
expect(screen.getByText("process.measurement.recorded.v1")).toBeInTheDocument();
158+
const timeline = screen.getByRole("list", {name: "Case replay event timeline"});
159+
const inspector = screen.getByRole("article", {name: "Selected replay event"});
160+
const metadata = screen.getByRole("region", {name: "Selected source event metadata"});
161+
const payload = screen.getByRole("region", {name: "Recorded payload"});
162+
expect(timeline.parentElement).toHaveClass("replay-trace-layout");
163+
expect(inspector).toContainElement(metadata);
164+
expect(inspector).toContainElement(payload);
165+
expect(metadata).toHaveTextContent("local-event-adapter");
166+
expect(payload).toHaveTextContent("Recorded payload");
158167
fireEvent.click(screen.getByRole("button", {name: "Next"}));
159168
expect(screen.getByRole("heading", {name: "case.detected"})).toBeInTheDocument();
160169
expect(screen.getByText("decision-audit")).toBeInTheDocument();
170+
expect(metadata).toHaveTextContent("decision-audit");
171+
expect(payload).not.toHaveTextContent('"sensor_name": "rf_power"');
161172
});
162173
});
163174

‎systems/web/src/screens.tsx‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -763,17 +763,19 @@ export function ReplayOperations({replay, trace = null}: {replay: ReplayResponse
763763
<span>{item.phase.replaceAll("_", " ")}</span><strong>{item.event_type.replaceAll(".v1", "")}</strong><small>{item.event_time ? item.event_time.slice(11, 19) : `#${item.sequence} order`}</small>
764764
</button>
765765
</li>)}</ol>
766-
<article className="replay-event-inspector">
766+
<article className="replay-event-inspector" aria-label="Selected replay event">
767767
{replayItem ? <>
768-
<div className="replay-event-inspector__title"><span>{replayItem.kind.replaceAll("_", " ")}</span><h3>{replayItem.event_type}</h3><small>{replayItem.event_time ?? "No persisted wall-clock timestamp"}</small></div>
769-
<dl>
770-
<div><dt>Phase</dt><dd>{replayItem.phase}</dd></div>
771-
<div><dt>Source</dt><dd>{replayItem.source}</dd></div>
772-
<div><dt>Time semantics</dt><dd>{replayItem.time_semantics.replaceAll("_", " ")}</dd></div>
773-
<div><dt>Delivery</dt><dd>{replayItem.delivery_status ?? "not applicable"}</dd></div>
774-
<div><dt>Event ID</dt><dd>{replayItem.event_id ?? "not an authoritative source event"}</dd></div>
775-
</dl>
776-
<div className="replay-payload"><span>Recorded payload</span><pre>{JSON.stringify(replayItem.payload, null, 2)}</pre></div>
768+
<section className="replay-event-metadata" aria-label="Selected source event metadata">
769+
<div className="replay-event-inspector__title"><span>{replayItem.kind.replaceAll("_", " ")}</span><h3>{replayItem.event_type}</h3><small>{replayItem.event_time ?? "No persisted wall-clock timestamp"}</small></div>
770+
<dl>
771+
<div><dt>Phase</dt><dd>{replayItem.phase}</dd></div>
772+
<div><dt>Source</dt><dd>{replayItem.source}</dd></div>
773+
<div><dt>Time semantics</dt><dd>{replayItem.time_semantics.replaceAll("_", " ")}</dd></div>
774+
<div><dt>Delivery</dt><dd>{replayItem.delivery_status ?? "not applicable"}</dd></div>
775+
<div><dt>Event ID</dt><dd>{replayItem.event_id ?? "not an authoritative source event"}</dd></div>
776+
</dl>
777+
</section>
778+
<section className="replay-payload" aria-label="Recorded payload"><span>Recorded payload</span><pre>{JSON.stringify(replayItem.payload, null, 2)}</pre></section>
777779
</> : <p>No replay item is available for the selected case.</p>}
778780
</article>
779781
</div>

0 commit comments

Comments
 (0)