Files
molecule-core/scripts/wheel_smoke.py
Molecule AI Dev Engineer A (Kimi) a4bb9f656a
gate-check-v3 / gate-check (pull_request) Successful in 9s
security-review / approved (pull_request) Failing after 5s
qa-review / approved (pull_request) Failing after 5s
sop-checklist / review-refire (pull_request) Has been skipped
sop-tier-check / tier-check (pull_request) Successful in 5s
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
sop-tier-check / tier-check (pull_request_review) Has been cancelled
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 11s
CI / Python Lint & Test (pull_request) Successful in 3s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
Check migration collisions / Migration version collision check (pull_request) Successful in 6s
E2E API Smoke Test / detect-changes (pull_request) Successful in 7s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 3s
E2E Chat / detect-changes (pull_request) Successful in 6s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 5s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
gate-check-v3 / gate-check (pull_request_target) Successful in 3s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 13s
CI / Detect changes (pull_request) Successful in 18s
qa-review / approved (pull_request_target) Successful in 4s
sop-checklist / review-refire (pull_request_target) Has been skipped
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request_target) Successful in 3s
security-review / approved (pull_request_target) Successful in 15s
sop-tier-check / tier-check (pull_request_target) Successful in 13s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 59s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 57s
E2E Chat / E2E Chat (pull_request) Successful in 2s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 1s
CI / Platform (Go) (pull_request) Successful in 1s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 9s
CI / Canvas (Next.js) (pull_request) Successful in 3s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Shellcheck (E2E scripts) (pull_request) Successful in 9s
CI / all-required (pull_request) Successful in 3m46s
audit-force-merge / audit (pull_request_target) Successful in 17s
chore(ci): add line-local rationales for lint/type suppressions (mc#1769)
Moves nearby block-comments onto the suppression lines so future
reviewers can see WHY each noqa/type: ignore is safe without hunting
through surrounding paragraphs.

Files:
- .gitea/scripts/sop-checklist.py     — type: ignore[import-not-found]
- scripts/ops/check_migration_collisions.py — noqa: S310
- scripts/wheel_smoke.py              — noqa: F401 (x5)

Closes #1769
2026-05-27 20:33:06 +00:00

174 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""Smoke-test an installed molecule-ai-workspace-runtime wheel.
Runs the same invariant assertions in two workflows:
* publish-runtime.yml — after building dist/*.whl, before PyPI upload
* runtime-prbuild-compat.yml — after building the PR's wheel, before merge
Splitting the smoke across two inline heredocs let PR-time and publish-time
drift apart. After 2026-04 we kept hitting publish-time failures for
regressions a PR-time check could have caught. One script, both gates.
Failure here intentionally exits non-zero so the workflow's `run:` step fails.
Each block prints a single ✓ line on success so the GH summary log stays
readable; assertion errors propagate with their own message.
Run directly: `python scripts/wheel_smoke.py` after `pip install <wheel>`.
"""
import os
import sys
def smoke_imports_and_invariants() -> None:
"""Module imports + stable contract assertions.
Importing main_sync by name is the strongest pre-PyPI gate we have for
import-rewrite mistakes (the 0.1.16 incident, where main.py loaded but
main_sync was missing because the build script dropped a re-export).
"""
from molecule_runtime.main import main_sync # noqa: F401 # smoke-test re-export regression (mc#1769)
from molecule_runtime import a2a_client, a2a_tools # noqa: F401 # smoke-test re-export regression (mc#1769)
from molecule_runtime.builtin_tools import memory # noqa: F401 # smoke-test re-export regression (mc#1769)
from molecule_runtime.adapters import get_adapter, BaseAdapter, AdapterConfig
# cli_main + mcp_cli.main are the molecule-mcp console-script entry
# points — the external-runtime universal MCP path. Same regression
# class as the 0.1.16 main_sync incident: a silent rename or missed
# rewrite here would break every external operator's MCP install on
# the next wheel publish. Pin both names because pyproject points
# at mcp_cli.main, which then imports a2a_mcp_server.cli_main.
from molecule_runtime.a2a_mcp_server import cli_main # noqa: F401 # smoke-test re-export regression (mc#1769)
from molecule_runtime.mcp_cli import main as mcp_cli_main # noqa: F401 # smoke-test re-export regression (mc#1769)
assert callable(cli_main), "a2a_mcp_server.cli_main must be callable"
assert callable(mcp_cli_main), "mcp_cli.main must be callable"
# inbox.activate / get_state / start_poller_thread form the inbound
# delivery path for the standalone molecule-mcp wrapper. mcp_cli.main
# imports + activates these at startup; if a wheel ships without
# them, the standalone agent silently loses the wait_for_message /
# inbox_peek / inbox_pop tools and reverts to outbound-only.
from molecule_runtime.inbox import ( # noqa: F401 # smoke-test re-export regression (mc#1769)
InboxState,
activate as inbox_activate,
get_state as inbox_get_state,
set_notification_callback as inbox_set_notification_callback,
start_poller_thread as inbox_start_poller_thread,
)
assert callable(inbox_activate), "inbox.activate must be callable"
assert callable(inbox_get_state), "inbox.get_state must be callable"
assert callable(inbox_start_poller_thread), "inbox.start_poller_thread must be callable"
assert callable(inbox_set_notification_callback), "inbox.set_notification_callback must be callable"
assert a2a_client._A2A_ERROR_PREFIX, "a2a_client missing error sentinel"
assert callable(get_adapter), "adapters.get_adapter must be callable"
assert hasattr(BaseAdapter, "name"), "BaseAdapter interface broken"
assert hasattr(AdapterConfig, "__init__"), "AdapterConfig dataclass missing"
print("✓ module imports + invariants OK")
def smoke_agent_card_call_shape() -> None:
"""Construct AgentCard with the EXACT kwargs main.py uses.
Pure imports don't catch field-shape regressions in upstream SDKs that
only surface at construction time. Two bugs of this exact class shipped
since the a2a-sdk 1.0 migration:
- state_transition_history=True (#2179)
- supported_protocols=[...] (the protobuf field is supported_interfaces;
every workspace boot crashed with `ValueError: Protocol message
AgentCard has no "supported_protocols" field`)
main.py and this block MUST stay in lockstep — adding a kwarg there
without mirroring it here is the regression vector.
"""
from a2a.types import AgentCard, AgentCapabilities, AgentSkill, AgentInterface
AgentCard(
name="smoke-agent",
description="wheel-smoke: AgentCard call-shape",
version="0.0.0-smoke",
supported_interfaces=[
AgentInterface(protocol_binding="https://a2a.g/v1", url="http://localhost:8080"),
],
capabilities=AgentCapabilities(
streaming=True,
push_notifications=False,
),
skills=[
AgentSkill(
id="smoke-skill",
name="Smoke",
description="no-op",
tags=["smoke"],
examples=["noop"],
),
],
default_input_modes=["text/plain", "application/json"],
default_output_modes=["text/plain", "application/json"],
)
print("✓ AgentCard call-shape smoke passed")
def smoke_well_known_path_alignment() -> None:
"""The SDK's published constant must match the path it actually mounts.
main.py polls AGENT_CARD_WELL_KNOWN_PATH to detect server readiness. If
the constant and create_agent_card_routes() drift, every workspace's
initial_prompt silently drops (probe 404s, falls through to "skipping").
This was the #2193 incident class.
"""
from a2a.types import AgentCard
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
from a2a.server.routes import create_agent_card_routes
mounted_paths = [
getattr(r, "path", None)
for r in create_agent_card_routes(
AgentCard(
name="wk-smoke",
description="well-known mount alignment",
version="0.0.0-smoke",
)
)
]
assert AGENT_CARD_WELL_KNOWN_PATH in mounted_paths, (
f"AGENT_CARD_WELL_KNOWN_PATH ({AGENT_CARD_WELL_KNOWN_PATH!r}) is NOT among "
f"paths mounted by create_agent_card_routes ({mounted_paths!r}). The SDK "
"constant and its own route factory have drifted — workspace probes will "
"404 forever, silently dropping every workspace initial_prompt."
)
print(f"✓ well-known mount alignment OK ({AGENT_CARD_WELL_KNOWN_PATH})")
def smoke_message_helper() -> None:
"""new_text_message is the v1.x rename of new_agent_text_message.
main.py and a2a_executor.py call new_text_message in hot paths; if the
import breaks, every reply errors with ImportError before the message
even leaves the workspace. Importing here catches a future v2.x rename
at publish time.
"""
from a2a.helpers import new_text_message
msg = new_text_message("smoke")
assert msg is not None, "new_text_message returned None"
print("✓ message helper import + call OK")
def main() -> int:
# main.py validates WORKSPACE_ID at module-import time via platform_auth.
# Set placeholders so the smoke doesn't trip on the env-var guard.
os.environ.setdefault("WORKSPACE_ID", "00000000-0000-0000-0000-000000000000")
os.environ.setdefault("PLATFORM_URL", "http://localhost:8080")
smoke_imports_and_invariants()
smoke_agent_card_call_shape()
smoke_well_known_path_alignment()
smoke_message_helper()
print("✓ wheel smoke passed")
return 0
if __name__ == "__main__":
sys.exit(main())