Forked clean from public hackathon repo (Starfire-AgentTeam, BSL 1.1) with full rebrand to Molecule AI under github.com/Molecule-AI/molecule-monorepo. Brand: Starfire → Molecule AI. Slug: starfire / agent-molecule → molecule. Env vars: STARFIRE_* → MOLECULE_*. Go module: github.com/agent-molecule/platform → github.com/Molecule-AI/molecule-monorepo/platform. Python packages: starfire_plugin → molecule_plugin, starfire_agent → molecule_agent. DB: agentmolecule → molecule. History truncated; see public repo for prior commits and contributor attribution. Verified green: go test -race ./... (platform), pytest (workspace-template 1129 + sdk 132), vitest (canvas 352), build (mcp). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
245 lines
10 KiB
Bash
Executable File
245 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# E2E test: Create a team with different templates/models, test A2A communication.
|
|
# Everything via platform API — no manual file edits.
|
|
set -euo pipefail
|
|
|
|
PLATFORM="${1:-http://localhost:8080}"
|
|
OR_KEY="${OPENAI_API_KEY:-${OPENROUTER_API_KEY:?Set OPENAI_API_KEY or OPENROUTER_API_KEY env var}}"
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
check() {
|
|
local label="$1" expected="$2" actual="$3"
|
|
if echo "$actual" | grep -q "$expected"; then
|
|
echo "PASS: $label"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "FAIL: $label"
|
|
echo " expected: $expected"
|
|
echo " got: $actual"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
wait_online() {
|
|
local id="$1" name="$2"
|
|
for i in $(seq 1 20); do
|
|
local s
|
|
s=$(curl -s "$PLATFORM/workspaces/$id" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null)
|
|
[ "$s" = "online" ] && return 0
|
|
sleep 3
|
|
done
|
|
echo " WARNING: $name did not come online within 60s"
|
|
return 1
|
|
}
|
|
|
|
echo "============================================"
|
|
echo " E2E Test: Multi-Template Team + A2A"
|
|
echo "============================================"
|
|
echo ""
|
|
|
|
# -------------------------------------------------------
|
|
# Step 1: Create workspaces from different templates
|
|
# -------------------------------------------------------
|
|
echo "--- Step 1: Create workspaces ---"
|
|
|
|
# PM — Claude Code (uses OAuth token from template)
|
|
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
|
|
-d '{"name":"PM","role":"Project Manager — coordinates the team","tier":2,"template":"claude-code-default"}')
|
|
PM_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
|
check "Create PM (claude-code)" "provisioning" "$R"
|
|
|
|
# Research Agent — LangGraph + Gemini Flash
|
|
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
|
|
-d '{"name":"Researcher","role":"Deep research and analysis","tier":2,"template":"langgraph"}')
|
|
RES_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
|
check "Create Researcher (langgraph)" "provisioning" "$R"
|
|
|
|
# Dev Agent — OpenClaw + Gemini Flash
|
|
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
|
|
-d '{"name":"Developer","role":"Code implementation and review","tier":2,"template":"openclaw"}')
|
|
DEV_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
|
check "Create Developer (openclaw)" "provisioning" "$R"
|
|
|
|
# Analyst — DeepAgents + Gemini Flash
|
|
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
|
|
-d '{"name":"Analyst","role":"Data analysis and reporting","tier":2,"template":"deepagents"}')
|
|
ANA_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
|
check "Create Analyst (deepagents)" "provisioning" "$R"
|
|
|
|
echo ""
|
|
echo " PM: $PM_ID"
|
|
echo " Researcher: $RES_ID"
|
|
echo " Developer: $DEV_ID"
|
|
echo " Analyst: $ANA_ID"
|
|
|
|
# -------------------------------------------------------
|
|
# Step 2: Set parent hierarchy via API
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "--- Step 2: Set parent hierarchy ---"
|
|
|
|
for CHILD_ID in $RES_ID $DEV_ID $ANA_ID; do
|
|
R=$(curl -s -X PATCH "$PLATFORM/workspaces/$CHILD_ID" -H 'Content-Type: application/json' \
|
|
-d "{\"parent_id\":\"$PM_ID\"}")
|
|
check "Set parent for $(echo $CHILD_ID | cut -c1-8)..." "updated" "$R"
|
|
done
|
|
|
|
# -------------------------------------------------------
|
|
# Step 3: Set secrets via API (OpenRouter key for non-Claude agents)
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "--- Step 3: Set API keys via secrets API ---"
|
|
|
|
for CHILD_ID in $RES_ID $DEV_ID $ANA_ID; do
|
|
R=$(curl -s -X POST "$PLATFORM/workspaces/$CHILD_ID/secrets" -H 'Content-Type: application/json' \
|
|
-d "{\"key\":\"OPENROUTER_API_KEY\",\"value\":\"$OR_KEY\"}")
|
|
check "Set OPENROUTER_API_KEY for $(echo $CHILD_ID | cut -c1-8)..." "saved" "$R"
|
|
done
|
|
|
|
# -------------------------------------------------------
|
|
# Step 4: Customize system prompts via Files API
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "--- Step 4: Wait for all to come online ---"
|
|
|
|
for name_id in "PM:$PM_ID" "Researcher:$RES_ID" "Developer:$DEV_ID" "Analyst:$ANA_ID"; do
|
|
IFS=: read name id <<< "$name_id"
|
|
if wait_online "$id" "$name"; then
|
|
check "$name online" "online" "online"
|
|
else
|
|
check "$name online" "online" "timeout"
|
|
fi
|
|
done
|
|
|
|
# -------------------------------------------------------
|
|
# Step 5: Customize prompts via Files API (containers are online now)
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "--- Step 5: Customize prompts via Files API ---"
|
|
|
|
R=$(curl -s -X PUT "$PLATFORM/workspaces/$RES_ID/files/system-prompt.md" -H 'Content-Type: application/json' \
|
|
-d '{"content":"You are a research agent. When asked to introduce yourself, say: I am the Researcher agent."}')
|
|
check "Set Researcher prompt" "saved" "$R"
|
|
|
|
R=$(curl -s -X PUT "$PLATFORM/workspaces/$DEV_ID/files/SOUL.md" -H 'Content-Type: application/json' \
|
|
-d '{"content":"You are a developer agent. When asked to introduce yourself, say: I am the Developer agent."}')
|
|
check "Set Developer prompt (SOUL.md)" "saved" "$R"
|
|
|
|
R=$(curl -s -X PUT "$PLATFORM/workspaces/$ANA_ID/files/system-prompt.md" -H 'Content-Type: application/json' \
|
|
-d '{"content":"You are an analyst agent. When asked to introduce yourself, say: I am the Analyst agent."}')
|
|
check "Set Analyst prompt" "saved" "$R"
|
|
|
|
# -------------------------------------------------------
|
|
# Step 6: Restart to pick up new prompts + secrets
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "--- Step 6: Restart agents ---"
|
|
|
|
for ID in $RES_ID $DEV_ID $ANA_ID; do
|
|
curl -s -X POST "$PLATFORM/workspaces/$ID/restart" > /dev/null
|
|
done
|
|
echo "Restarting 3 agents... waiting 30s"
|
|
sleep 30
|
|
|
|
for name_id in "Researcher:$RES_ID" "Developer:$DEV_ID" "Analyst:$ANA_ID"; do
|
|
IFS=: read name id <<< "$name_id"
|
|
if wait_online "$id" "$name"; then
|
|
check "$name back online" "online" "online"
|
|
else
|
|
check "$name back online" "online" "timeout"
|
|
fi
|
|
done
|
|
|
|
# -------------------------------------------------------
|
|
# Step 7: Verify files in containers (no host ws-* dirs)
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "--- Step 7: Verify config files in containers ---"
|
|
|
|
R=$(curl -s "$PLATFORM/workspaces/$RES_ID/files" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))")
|
|
check "Researcher has files" "[12]" "$R"
|
|
|
|
R=$(curl -s "$PLATFORM/workspaces/$DEV_ID/files" | python3 -c "import sys,json; files=json.load(sys.stdin); print(' '.join(f['path'] for f in files if not f['dir']))")
|
|
check "Developer has OpenClaw files" "SOUL.md" "$R"
|
|
|
|
# Verify NO ws-* dirs on host
|
|
HOST_WS=$(find /Users/hongming/Documents/GitHub/molecule-monorepo/workspace-configs-templates -maxdepth 1 -name 'ws-*' -type d 2>/dev/null | wc -l | tr -d ' ')
|
|
check "No ws-* dirs on host" "0" "$HOST_WS"
|
|
|
|
# -------------------------------------------------------
|
|
# Step 8: Test A2A — direct messages
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "--- Step 8: A2A direct messages ---"
|
|
|
|
# Talk to Researcher
|
|
R=$(curl -s -X POST "$PLATFORM/workspaces/$RES_ID/a2a" -H 'Content-Type: application/json' \
|
|
-d '{"method":"message/send","params":{"message":{"role":"user","parts":[{"kind":"text","text":"introduce yourself in one sentence"}]}}}')
|
|
RESP=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin).get('result',{}).get('parts',[{}])[0].get('text','ERROR')[:200])" 2>/dev/null)
|
|
echo " Researcher says: $RESP"
|
|
check "Researcher responds" "Researcher" "$RESP"
|
|
|
|
# Talk to Developer
|
|
R=$(curl -s -X POST "$PLATFORM/workspaces/$DEV_ID/a2a" -H 'Content-Type: application/json' \
|
|
-d '{"method":"message/send","params":{"message":{"role":"user","parts":[{"kind":"text","text":"introduce yourself in one sentence"}]}}}')
|
|
RESP=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin).get('result',{}).get('parts',[{}])[0].get('text','ERROR')[:200])" 2>/dev/null)
|
|
echo " Developer says: $RESP"
|
|
check "Developer responds" "Developer" "$RESP"
|
|
|
|
# Talk to Analyst
|
|
R=$(curl -s -X POST "$PLATFORM/workspaces/$ANA_ID/a2a" -H 'Content-Type: application/json' \
|
|
-d '{"method":"message/send","params":{"message":{"role":"user","parts":[{"kind":"text","text":"introduce yourself in one sentence"}]}}}')
|
|
RESP=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin).get('result',{}).get('parts',[{}])[0].get('text','ERROR')[:200])" 2>/dev/null)
|
|
echo " Analyst says: $RESP"
|
|
check "Analyst responds" "Analyst" "$RESP"
|
|
|
|
# -------------------------------------------------------
|
|
# Step 9: Test peer discovery
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "--- Step 9: Peer discovery ---"
|
|
|
|
R=$(curl -s "$PLATFORM/registry/${PM_ID}/peers" | python3 -c "import sys,json; peers=json.load(sys.stdin); print(len(peers))")
|
|
check "PM has 3 peers (children)" "3" "$R"
|
|
|
|
R=$(curl -s "$PLATFORM/registry/${RES_ID}/peers" | python3 -c "import sys,json; peers=json.load(sys.stdin); names=[p.get('name','') for p in peers]; print(' '.join(names))")
|
|
check "Researcher sees siblings" "Developer" "$R"
|
|
|
|
# -------------------------------------------------------
|
|
# Step 10: Test cross-workspace access control
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "--- Step 10: Access control ---"
|
|
|
|
# Parent <-> child (PM <-> Researcher) should be allowed
|
|
R=$(curl -s -X POST "$PLATFORM/registry/check-access" -H 'Content-Type: application/json' \
|
|
-d "{\"caller_id\":\"$PM_ID\",\"target_id\":\"$RES_ID\"}")
|
|
check "PM -> Researcher allowed" "true" "$R"
|
|
|
|
# Siblings (Researcher <-> Developer) should be allowed
|
|
R=$(curl -s -X POST "$PLATFORM/registry/check-access" -H 'Content-Type: application/json' \
|
|
-d "{\"caller_id\":\"$RES_ID\",\"target_id\":\"$DEV_ID\"}")
|
|
check "Researcher -> Developer allowed" "true" "$R"
|
|
|
|
# -------------------------------------------------------
|
|
# Step 11: Cleanup
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "--- Step 11: Cleanup ---"
|
|
|
|
curl -s -X DELETE "$PLATFORM/workspaces/$PM_ID" > /dev/null
|
|
check "Delete PM (cascades)" "ok" "ok"
|
|
|
|
REMAINING=$(curl -s "$PLATFORM/workspaces" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))")
|
|
check "All workspaces deleted" "0" "$REMAINING"
|
|
|
|
# -------------------------------------------------------
|
|
# Results
|
|
# -------------------------------------------------------
|
|
echo ""
|
|
echo "============================================"
|
|
echo " Results: $PASS passed, $FAIL failed"
|
|
echo "============================================"
|
|
exit $FAIL
|