install-canary #15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: install-canary | |
| # Verifies that what users actually receive from PyPI works: fresh install, | |
| # import, MCP stdio handshake, and a real remember/recall round trip. | |
| # Exists because mcp 2.0.0 broke every fresh install for weeks before anyone | |
| # noticed -- the repo's own CI installs from source and could not see it. | |
| on: | |
| schedule: | |
| - cron: "17 9 * * *" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| fresh-install: | |
| name: PyPI fresh install (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 40 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest] | |
| env: | |
| GENOME_MCP_DB: canary-memories.db | |
| HF_HUB_DISABLE_PROGRESS_BARS: "1" | |
| steps: | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.12" | |
| # No checkout on purpose: the job must see exactly what a user gets | |
| # from PyPI, independent of the repo state. | |
| - name: Fresh install from PyPI | |
| run: pip install --no-cache-dir "genome-memory[mcp]" | |
| - name: Import check | |
| run: python -c "import genome; from genome.mcp.server import main; from importlib.metadata import version; print('genome-memory', version('genome-memory'))" | |
| # Separate step so a slow model download fails distinctly from a broken | |
| # package (the cold torch import + download exceeded 10 min on a | |
| # 2-core Windows runner when folded into the round trip). | |
| - name: Pre-warm embedding model | |
| timeout-minutes: 20 | |
| run: python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')" | |
| - name: MCP stdio round trip | |
| timeout-minutes: 10 | |
| shell: bash | |
| run: | | |
| python - <<'PY' | |
| import json, subprocess, sys, threading, time | |
| proc = subprocess.Popen( | |
| ["genome-mcp"], | |
| stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
| text=True, encoding="utf-8", | |
| ) | |
| out_lines, err_chunks = [], [] | |
| threading.Thread( | |
| target=lambda: [out_lines.append(l) for l in proc.stdout], daemon=True | |
| ).start() | |
| # stderr must be drained or a chatty model download can deadlock the pipe | |
| threading.Thread( | |
| target=lambda: [err_chunks.append(l) for l in proc.stderr], daemon=True | |
| ).start() | |
| def send(msg): | |
| proc.stdin.write(json.dumps(msg) + "\n") | |
| proc.stdin.flush() | |
| def wait_for(rpc_id, deadline_s): | |
| deadline = time.monotonic() + deadline_s | |
| while time.monotonic() < deadline: | |
| for line in list(out_lines): | |
| try: | |
| msg = json.loads(line) | |
| except ValueError: | |
| continue | |
| if msg.get("id") == rpc_id: | |
| return msg | |
| if proc.poll() is not None: | |
| break | |
| time.sleep(0.2) | |
| return None | |
| def call_tool(rpc_id, name, arguments, deadline_s): | |
| send({"jsonrpc": "2.0", "id": rpc_id, "method": "tools/call", | |
| "params": {"name": name, "arguments": arguments}}) | |
| resp = wait_for(rpc_id, deadline_s) | |
| assert resp and "result" in resp, f"{name} failed: {resp}" | |
| assert not resp["result"].get("isError"), f"{name} errored: {resp}" | |
| return "".join( | |
| c.get("text", "") for c in resp["result"].get("content", []) | |
| ) | |
| try: | |
| send({"jsonrpc": "2.0", "id": 1, "method": "initialize", | |
| "params": {"protocolVersion": "2024-11-05", "capabilities": {}, | |
| "clientInfo": {"name": "install-canary", "version": "0"}}}) | |
| resp = wait_for(1, 120) | |
| assert resp and "result" in resp, f"no initialize result: {resp}" | |
| print("serverInfo:", resp["result"].get("serverInfo")) | |
| send({"jsonrpc": "2.0", "method": "notifications/initialized"}) | |
| send({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}) | |
| resp = wait_for(2, 60) | |
| assert resp and "result" in resp, f"no tools/list result: {resp}" | |
| tools = sorted(t["name"] for t in resp["result"]["tools"]) | |
| print("tools:", tools) | |
| assert "remember" in tools and "recall" in tools, tools | |
| # First remember triggers the embedding model download in a fresh env | |
| text = call_tool(3, "remember", | |
| {"content": "The canary token is GENOME-CANARY-4417.", | |
| "user_id": "canary"}, 600) | |
| print("remember:", text) | |
| text = call_tool(4, "recall", | |
| {"query": "what is the canary token?", | |
| "user_id": "canary"}, 120) | |
| print("recall:", text) | |
| assert "GENOME-CANARY-4417" in text, f"stored fact not recalled: {text!r}" | |
| print("CANARY PASS") | |
| finally: | |
| proc.kill() | |
| tail = "".join(err_chunks)[-4000:] | |
| if tail: | |
| sys.stderr.write(tail) | |
| PY |