test(mcp): exercise real MCPServerAdaptor in contract producer test (core#3080) #3100
+40
-2
@@ -165,10 +165,48 @@ jobs:
|
||||
# go-arch-lint "without types"). Fleet sweep after the cp ci.yml find.
|
||||
cache: false
|
||||
- if: ${{ needs.changes.outputs.platform == 'true' }}
|
||||
name: Install workspace-runtime wheel for contract tests
|
||||
# CR2 #12653 fix: the previous step tried to `pip install` the runtime
|
||||
# wheel and used `|| true` to swallow install failures. With
|
||||
# AUTO_SYNC_TOKEN lacking the package-install scope (HTTP 401),
|
||||
# the install silently failed; the contract test then SKIPPED with
|
||||
# "runtime not available" — a false-green because the green
|
||||
# Platform (Go) job was NOT exercising the real MCPServerAdaptor.
|
||||
#
|
||||
# The contract test imports the runtime from its SOURCE TREE
|
||||
# (PYTHONPATH=<runtime>/) so the settings-fragment.json layout is
|
||||
# preserved. The runtime's own import chain pulls `a2a-sdk` (and
|
||||
# other declared deps) — those MUST be available in the Python env
|
||||
# where the test runs, otherwise `import molecule_runtime.*`
|
||||
# fails with `ModuleNotFoundError: No module named 'a2a'`. We
|
||||
# therefore `pip install` the runtime package from the checked-out
|
||||
# source (no `|| true` — failures must be loud). The pip install
|
||||
# resolves deps from PyPI (not the org package index that
|
||||
# AUTO_SYNC_TOKEN couldn't reach) and the test's explicit
|
||||
# PYTHONPATH override keeps the source on the import path so the
|
||||
# source wins over the site-packages copy.
|
||||
name: Checkout molecule-ai-workspace-runtime source for contract tests
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
repository: molecule-ai/molecule-ai-workspace-runtime
|
||||
path: molecule-ai-workspace-runtime
|
||||
token: ${{ secrets.AUTO_SYNC_TOKEN }}
|
||||
- if: ${{ needs.changes.outputs.platform == 'true' }}
|
||||
name: Install molecule-ai-workspace-runtime and its Python deps for contract tests
|
||||
working-directory: ./molecule-ai-workspace-runtime
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Ensure pip is available (bare ubuntu-latest runners may not have it
|
||||
# by default; `ensurepip` is the standard-library bootstrap).
|
||||
if ! python3 -m pip --version >/dev/null 2>&1; then
|
||||
python3 -m ensurepip --upgrade
|
||||
fi
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install .
|
||||
- if: ${{ needs.changes.outputs.platform == 'true' }}
|
||||
name: Export MOLECULE_WORKSPACE_RUNTIME for contract tests
|
||||
working-directory: .
|
||||
run: |
|
||||
pip install --index-url "https://agent-dev-a:${{ secrets.AUTO_SYNC_TOKEN }}@git.moleculesai.app/api/packages/molecule-ai/pypi/simple/" molecule-ai-workspace-runtime || true
|
||||
echo "MOLECULE_WORKSPACE_RUNTIME=$GITHUB_WORKSPACE/molecule-ai-workspace-runtime" >> "$GITHUB_ENV"
|
||||
- if: ${{ needs.changes.outputs.platform == 'true' }}
|
||||
run: go mod download
|
||||
- if: ${{ needs.changes.outputs.platform == 'true' }}
|
||||
|
||||
@@ -3,7 +3,6 @@ package handlers
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -11,51 +10,6 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mergeMCPPluginSettings models the runtime MCPServerAdaptor's write side for
|
||||
// the contract test: it reads a plugin's settings-fragment.json and merges its
|
||||
// mcpServers block into the contract settings_path/key. Keeping this logic in
|
||||
// the test file makes the contract test hermetic and avoids adding dead
|
||||
// production code; the live pipeline still uses the Python runtime adaptor.
|
||||
func mergeMCPPluginSettings(configsDir, pluginRoot string, contract *MCPPluginDeliveryContract) error {
|
||||
data, err := os.ReadFile(filepath.Join(pluginRoot, "settings-fragment.json"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read settings-fragment.json: %w", err)
|
||||
}
|
||||
var fragment map[string]any
|
||||
if err := json.Unmarshal(data, &fragment); err != nil {
|
||||
return fmt.Errorf("parse settings-fragment.json: %w", err)
|
||||
}
|
||||
mcpServers, ok := fragment[contract.Key].(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("settings-fragment.json %q key missing or not an object", contract.Key)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(contract.SettingsPath, "/configs/") {
|
||||
return fmt.Errorf("contract settings_path %q does not start with /configs/", contract.SettingsPath)
|
||||
}
|
||||
rel := strings.TrimPrefix(contract.SettingsPath, "/configs/")
|
||||
settingsPath := filepath.Join(configsDir, rel)
|
||||
if err := os.MkdirAll(filepath.Dir(settingsPath), 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir settings dir: %w", err)
|
||||
}
|
||||
|
||||
existing := map[string]any{}
|
||||
if cur, err := os.ReadFile(settingsPath); err == nil {
|
||||
_ = json.Unmarshal(cur, &existing)
|
||||
}
|
||||
existing[contract.Key] = mcpServers
|
||||
|
||||
out, err := json.MarshalIndent(existing, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal settings: %w", err)
|
||||
}
|
||||
out = append(out, '\n')
|
||||
if err := os.WriteFile(settingsPath, out, 0o644); err != nil {
|
||||
return fmt.Errorf("write settings: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestMCPPluginDeliveryContract_MatchesSSOT pins the producer side of the
|
||||
// MCP-plugin delivery contract. The contract file is the SSOT shared with
|
||||
// molecule-ai-workspace-template-claude-code; any change to the pinned path,
|
||||
@@ -93,74 +47,17 @@ func TestMCPPluginDeliveryContract_LoadableFromRepoRoot(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestMCPPluginDeliveryContract_MCPServerAdaptorWritesMcpServers asserts the
|
||||
// producer side of the contract: an MCP-server plugin's settings-fragment.json
|
||||
// is merged into the exact settings_path and key pinned by the contract. This
|
||||
// prevents silent divergence between where MCPServerAdaptor writes and where
|
||||
// claude_sdk_executor._load_settings_mcp reads.
|
||||
// producer side of the contract by exercising the REAL production
|
||||
// MCPServerAdaptor from molecule-ai-workspace-runtime. It merges an MCP-server
|
||||
// plugin's settings-fragment.json into the exact settings_path and key pinned
|
||||
// by the contract. This catches real producer drift; the previous test-local
|
||||
// helper that modelled the adaptor has been removed.
|
||||
func TestMCPPluginDeliveryContract_MCPServerAdaptorWritesMcpServers(t *testing.T) {
|
||||
contract, err := LoadMCPPluginDeliveryContract()
|
||||
if err != nil {
|
||||
t.Fatalf("load contract: %v", err)
|
||||
}
|
||||
|
||||
configsDir := t.TempDir()
|
||||
pluginRoot := t.TempDir()
|
||||
|
||||
fragment := map[string]any{
|
||||
contract.Key: map[string]any{
|
||||
"molecule-platform": map[string]any{
|
||||
"command": "molecule-mcp",
|
||||
"env": map[string]string{
|
||||
"MOLECULE_MCP_MODE": "management",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
fragmentBytes, err := json.Marshal(fragment)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal fragment: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(pluginRoot, "settings-fragment.json"), fragmentBytes, 0o644); err != nil {
|
||||
t.Fatalf("write settings-fragment.json: %v", err)
|
||||
}
|
||||
|
||||
if err := mergeMCPPluginSettings(configsDir, pluginRoot, contract); err != nil {
|
||||
t.Fatalf("merge mcp plugin settings: %v", err)
|
||||
}
|
||||
|
||||
rel := strings.TrimPrefix(contract.SettingsPath, "/configs/")
|
||||
settingsPath := filepath.Join(configsDir, rel)
|
||||
gotBytes, err := os.ReadFile(settingsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read produced settings at %s: %v", contract.SettingsPath, err)
|
||||
}
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(gotBytes, &got); err != nil {
|
||||
t.Fatalf("parse produced settings: %v", err)
|
||||
}
|
||||
if _, ok := got[contract.Key]; !ok {
|
||||
t.Fatalf("produced settings missing contract key %q", contract.Key)
|
||||
}
|
||||
mcpServers, ok := got[contract.Key].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("produced settings %q is not an object: %T", contract.Key, got[contract.Key])
|
||||
}
|
||||
if _, ok := mcpServers["molecule-platform"]; !ok {
|
||||
t.Fatalf("produced settings %q does not contain the molecule-platform entry", contract.Key)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMCPPluginDeliveryContract_RealMCPServerAdaptorWritesMcpServers exercises
|
||||
// the actual runtime MCPServerAdaptor when molecule-ai-workspace-runtime is
|
||||
// available. This is the real producer named in the contract; the hermetic test
|
||||
// above is the always-on CI guard, while this test catches drift in the runtime
|
||||
// implementation itself.
|
||||
func TestMCPPluginDeliveryContract_RealMCPServerAdaptorWritesMcpServers(t *testing.T) {
|
||||
contract, err := LoadMCPPluginDeliveryContract()
|
||||
if err != nil {
|
||||
t.Fatalf("load contract: %v", err)
|
||||
}
|
||||
|
||||
runtimePath := os.Getenv("MOLECULE_WORKSPACE_RUNTIME")
|
||||
if runtimePath == "" {
|
||||
// Default sibling checkout relative to the core repo root.
|
||||
@@ -170,6 +67,7 @@ func TestMCPPluginDeliveryContract_RealMCPServerAdaptorWritesMcpServers(t *testi
|
||||
runtimePath = sibling
|
||||
}
|
||||
}
|
||||
|
||||
pyScript := `
|
||||
import asyncio, json, sys
|
||||
from pathlib import Path
|
||||
@@ -233,8 +131,19 @@ asyncio.run(main())
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
// CR2 #12653 fix: this test is the real-producer gate for the
|
||||
// MCP-plugin delivery contract. The previous behavior of
|
||||
// `t.Skipf` when runtimePath was empty turned the green Platform
|
||||
// (Go) job into a false-green whenever the runtime wasn't
|
||||
// checked out (HTTP 401 on the old `pip install ... || true` step).
|
||||
// The skip counted as a pass, so the test was not actually
|
||||
// exercising the real MCPServerAdaptor. A skip here is a
|
||||
// production-blocking false-green; the test must FAIL so the
|
||||
// missing runtime is visible in the gate.
|
||||
if runtimePath == "" {
|
||||
t.Skipf("molecule-ai-workspace-runtime not available (%v); skipping real-producer test", err)
|
||||
t.Fatalf("CR2 #12653: molecule-ai-workspace-runtime source not found — this test must exercise the REAL MCPServerAdaptor. "+
|
||||
"Set MOLECULE_WORKSPACE_RUNTIME=/path/to/molecule-ai-workspace-runtime or check out the runtime as a sibling of the repo root. "+
|
||||
"Underlying python error (if any): %v", err)
|
||||
}
|
||||
t.Fatalf("run MCPServerAdaptor: %v\nstderr: %s", err, stderr.String())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user