forked from molecule-ai/molecule-core
All 5 suggestions from the latest review pass.
## tests/e2e/_extract_token.py (new)
Extracted the 14-line python-in-bash heredoc from _lib.sh into a real
Python file. Easier to edit, fewer escaping traps, same behavior.
Shell helper now just shells out to it.
## tests/e2e/_lib.sh
- Replaced inline python with: python3 "$(dirname "${BASH_SOURCE[0]}")/_extract_token.py"
- Removed redundant sys.exit(0) as part of the extraction
## Shellcheck-clean scripts (new CI job enforces)
- Removed dead captures: BEFORE_COUNT (test_activity_e2e.sh), ORIG_SKILLS,
REIMPORT_SKILLS (test_api.sh), QA_TOKEN (test_comprehensive_e2e.sh)
- Renamed unused loop vars `i`, `j` -> `_` in 4 sites
- Added `# shellcheck disable=SC2046` on the two intentional word-splits
in test_claude_code_e2e.sh (docker stop/rm of multiple container IDs)
- Removed a useless re-register of QA mid-script (was done in Section 2)
## CI (.github/workflows/ci.yml)
- Replaced `sudo apt-get install postgresql-client` + psql with a direct
`docker exec` into the existing postgres:16 service container. Saves
~10-20s per CI run.
- Added new `shellcheck` job that lints tests/e2e/*.sh on every PR.
Local: shellcheck --severity=warning returns 0 across all 5 scripts.
## Verification
- go test -race ./internal/handlers/... : pass
- mcp-server: 96/96 jest
- canvas: 357/357 vitest + clean build
- tests/e2e/test_api.sh: 62/62
- tests/e2e/test_comprehensive_e2e.sh: 67/67
- shellcheck tests/e2e/*.sh : clean
- CI YAML: valid
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
588 lines
22 KiB
Bash
Executable File
588 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Comprehensive E2E test — covers ALL platform API endpoints, workspace lifecycle,
|
|
# parent-child A2A, peer delegation, secrets, config, bundles, approvals, memories, and more.
|
|
#
|
|
# Requires: platform running on :8080, Postgres + Redis up.
|
|
# Does NOT require running agent containers (tests platform-only behavior).
|
|
set -euo pipefail
|
|
|
|
source "$(dirname "$0")/_lib.sh" # sets BASE default
|
|
PASS=0
|
|
FAIL=0
|
|
SKIP=0
|
|
|
|
# Phase 30.1: tokens issued at /registry/register must be echoed back on
|
|
# heartbeat, update-card, discover, and peers calls.
|
|
PM_TOKEN=""
|
|
DEV_TOKEN=""
|
|
|
|
e2e_cleanup_all_workspaces
|
|
|
|
check() {
|
|
local desc="$1" expected="$2" actual="$3"
|
|
if echo "$actual" | grep -qF "$expected"; then
|
|
echo " PASS: $desc"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo " FAIL: $desc"
|
|
echo " expected: $expected"
|
|
echo " got: ${actual:0:200}"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
check_status() {
|
|
local desc="$1" expected_code="$2" actual_code="$3"
|
|
if [ "$actual_code" = "$expected_code" ]; then
|
|
echo " PASS: $desc (HTTP $actual_code)"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo " FAIL: $desc (expected HTTP $expected_code, got $actual_code)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
jq_extract() {
|
|
python3 -c "import sys,json; print(json.load(sys.stdin)$1)" 2>/dev/null
|
|
}
|
|
|
|
echo "============================================"
|
|
echo " Comprehensive Platform E2E Test Suite"
|
|
echo "============================================"
|
|
echo ""
|
|
|
|
# ============================================================
|
|
# Section 1: Health & Metrics
|
|
# ============================================================
|
|
echo "--- Section 1: Health & Metrics ---"
|
|
R=$(curl -s "$BASE/health")
|
|
check "GET /health" '"status":"ok"' "$R"
|
|
|
|
CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/metrics")
|
|
check_status "GET /metrics returns 200" "200" "$CODE"
|
|
|
|
# ============================================================
|
|
# Section 2: Workspace CRUD
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 2: Workspace CRUD ---"
|
|
|
|
# Create parent workspace (PM) and immediately register to capture its
|
|
# auth token BEFORE the provisioner's container can spawn and claim it.
|
|
# Tokens are single-issue on first /registry/register per workspace
|
|
# (Phase 30.1) — if the container's main.py beats us, our later calls
|
|
# get no token and bearer-protected endpoints fail. Empirically the
|
|
# script wins the race 5/5 times when register fires right after
|
|
# create; sections that depend on container readiness (RT_* in 2b)
|
|
# still run normally.
|
|
R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \
|
|
-d '{"name":"Test PM","role":"Project Manager","tier":2}')
|
|
check "Create PM" '"status":"provisioning"' "$R"
|
|
PM_ID=$(echo "$R" | jq_extract "['id']")
|
|
echo " PM_ID=$PM_ID"
|
|
RR=$(curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \
|
|
-d "{\"id\":\"$PM_ID\",\"url\":\"http://localhost:9000\",\"agent_card\":{\"name\":\"PM\",\"skills\":[]}}")
|
|
PM_TOKEN=$(echo "$RR" | e2e_extract_token)
|
|
|
|
# Create child workspace under PM
|
|
R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \
|
|
-d "{\"name\":\"Test Dev\",\"role\":\"Developer\",\"tier\":2,\"parent_id\":\"$PM_ID\"}")
|
|
check "Create Dev (child of PM)" '"status":"provisioning"' "$R"
|
|
DEV_ID=$(echo "$R" | jq_extract "['id']")
|
|
RR=$(curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \
|
|
-d "{\"id\":\"$DEV_ID\",\"url\":\"http://localhost:9001\",\"agent_card\":{\"name\":\"Dev Agent\",\"skills\":[],\"version\":\"1.0.0\"}}")
|
|
DEV_TOKEN=$(echo "$RR" | e2e_extract_token)
|
|
|
|
# Create sibling
|
|
R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \
|
|
-d "{\"name\":\"Test QA\",\"role\":\"QA\",\"tier\":1,\"parent_id\":\"$PM_ID\"}")
|
|
check "Create QA (sibling of Dev)" '"status":"provisioning"' "$R"
|
|
QA_ID=$(echo "$R" | jq_extract "['id']")
|
|
curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \
|
|
-d "{\"id\":\"$QA_ID\",\"url\":\"http://localhost:9002\",\"agent_card\":{\"name\":\"QA\",\"skills\":[]}}" > /dev/null
|
|
|
|
# Create unrelated workspace
|
|
R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \
|
|
-d '{"name":"Test Outsider","role":"External","tier":1}')
|
|
check "Create Outsider (unrelated)" '"status":"provisioning"' "$R"
|
|
OUTSIDER_ID=$(echo "$R" | jq_extract "['id']")
|
|
|
|
# List workspaces
|
|
R=$(curl -s "$BASE/workspaces")
|
|
check "List workspaces (4 total)" "$PM_ID" "$R"
|
|
|
|
# Get single workspace
|
|
R=$(curl -s "$BASE/workspaces/$PM_ID")
|
|
check "Get PM by ID" '"name":"Test PM"' "$R"
|
|
|
|
# Update workspace position
|
|
R=$(curl -s -X PATCH "$BASE/workspaces/$PM_ID" -H "Content-Type: application/json" \
|
|
-d '{"x":100,"y":200}')
|
|
check "Update PM position" '"status":"updated"' "$R"
|
|
|
|
# Verify position persisted
|
|
R=$(curl -s "$BASE/workspaces/$PM_ID")
|
|
check "PM position persisted" '"x":100' "$R"
|
|
|
|
# ============================================================
|
|
# Section 2b: Runtime Assignment & Image Selection
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 2b: Runtime Assignment ---"
|
|
|
|
# Create workspace with explicit runtime
|
|
R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \
|
|
-d '{"name":"RT Claude","role":"Test","tier":2,"runtime":"claude-code"}')
|
|
check "Create claude-code workspace" '"status":"provisioning"' "$R"
|
|
RT_CC_ID=$(echo "$R" | jq_extract "['id']")
|
|
|
|
R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \
|
|
-d '{"name":"RT LangGraph","role":"Test","tier":2,"runtime":"langgraph"}')
|
|
check "Create langgraph workspace" '"status":"provisioning"' "$R"
|
|
RT_LG_ID=$(echo "$R" | jq_extract "['id']")
|
|
|
|
R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \
|
|
-d '{"name":"RT CrewAI","role":"Test","tier":2,"runtime":"crewai"}')
|
|
check "Create crewai workspace" '"status":"provisioning"' "$R"
|
|
RT_CR_ID=$(echo "$R" | jq_extract "['id']")
|
|
|
|
# Wait for containers to start (poll up to 30s for first one to appear)
|
|
if command -v docker &>/dev/null; then
|
|
short_cc="${RT_CC_ID:0:12}"
|
|
for _ in 1 2 3 4 5 6; do
|
|
sleep 5
|
|
if docker inspect "ws-${short_cc}" >/dev/null 2>&1; then break; fi
|
|
done
|
|
|
|
_check_image() {
|
|
local ws_id="$1" expected_tag="$2" label="$3"
|
|
local short_id="${ws_id:0:12}"
|
|
# Poll up to 30s for image to appear
|
|
local actual_image="NOT_FOUND"
|
|
for _ in 1 2 3 4 5 6; do
|
|
actual_image=$(docker inspect "ws-${short_id}" --format '{{.Config.Image}}' 2>/dev/null || echo "NOT_FOUND")
|
|
if echo "$actual_image" | grep -qF "$expected_tag"; then break; fi
|
|
sleep 5
|
|
done
|
|
if echo "$actual_image" | grep -qF "$expected_tag"; then
|
|
echo " PASS: $label → $actual_image"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo " FAIL: $label (expected *$expected_tag, got $actual_image)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
_check_image "$RT_CC_ID" "claude-code" "claude-code uses claude-code image"
|
|
_check_image "$RT_LG_ID" "langgraph" "langgraph uses langgraph image"
|
|
_check_image "$RT_CR_ID" "crewai" "crewai uses crewai image"
|
|
else
|
|
echo " SKIP: Docker not available — cannot verify container images"
|
|
SKIP=$((SKIP + 3))
|
|
fi
|
|
|
|
# Verify runtime in agent card after registration
|
|
sleep 5
|
|
for rt_id in $RT_CC_ID $RT_LG_ID $RT_CR_ID; do
|
|
# Register so we can check agent card
|
|
curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \
|
|
-d "{\"id\":\"$rt_id\",\"url\":\"http://localhost:19999\",\"agent_card\":{\"name\":\"Test\",\"skills\":[]}}" > /dev/null 2>&1
|
|
done
|
|
|
|
# Config file should reflect runtime
|
|
R=$(curl -s "$BASE/workspaces/$RT_CC_ID/files/config.yaml" 2>/dev/null)
|
|
if echo "$R" | grep -qF "runtime: claude-code"; then
|
|
echo " PASS: claude-code config.yaml has runtime: claude-code"
|
|
PASS=$((PASS + 1))
|
|
elif echo "$R" | grep -qF "error"; then
|
|
echo " SKIP: config.yaml not accessible (container may not be ready)"
|
|
SKIP=$((SKIP + 1))
|
|
else
|
|
echo " FAIL: claude-code config.yaml missing runtime field"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
# Verify runtime change persists on restart (if provisioner supports ExecRead)
|
|
# Write a new runtime to config, restart, check image changes
|
|
R=$(curl -s -X PUT "$BASE/workspaces/$RT_LG_ID/files/config.yaml" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"content":"name: RT LangGraph\nruntime: deepagents\nmodel: openai:gpt-4.1-mini\ntier: 2\n"}')
|
|
if echo "$R" | grep -qF "saved"; then
|
|
curl -s -X POST "$BASE/workspaces/$RT_LG_ID/restart" > /dev/null 2>&1
|
|
# Poll up to 30s for the new container image to appear (restart can take a while)
|
|
if command -v docker &>/dev/null; then
|
|
short_id="${RT_LG_ID:0:12}"
|
|
for _ in 1 2 3 4 5 6; do
|
|
sleep 5
|
|
actual=$(docker inspect "ws-${short_id}" --format '{{.Config.Image}}' 2>/dev/null || echo "")
|
|
if echo "$actual" | grep -qF "deepagents"; then break; fi
|
|
done
|
|
_check_image "$RT_LG_ID" "deepagents" "Runtime change langgraph→deepagents on restart"
|
|
else
|
|
echo " SKIP: Docker not available"
|
|
SKIP=$((SKIP + 1))
|
|
fi
|
|
else
|
|
echo " SKIP: Could not write config (container offline)"
|
|
SKIP=$((SKIP + 1))
|
|
fi
|
|
|
|
# Clean up runtime test workspaces
|
|
for rt_id in $RT_CC_ID $RT_LG_ID $RT_CR_ID; do
|
|
curl -s -X DELETE "$BASE/workspaces/$rt_id?confirm=true" > /dev/null 2>&1
|
|
sleep 0.3
|
|
done
|
|
|
|
# ============================================================
|
|
# Section 3: Registry & Heartbeat
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 3: Registry & Heartbeat ---"
|
|
|
|
# Dev was already registered in Section 2 right after creation (to beat
|
|
# the provisioner in the token-issuance race). Re-assert the status here.
|
|
|
|
# Verify Dev is now online
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID")
|
|
check "Dev status online after register" '"status":"online"' "$R"
|
|
|
|
# Heartbeat with current_task
|
|
R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $DEV_TOKEN" \
|
|
-d "{\"workspace_id\":\"$DEV_ID\",\"active_tasks\":1,\"current_task\":\"Running tests\"}")
|
|
check "Heartbeat with task" '"status":"ok"' "$R"
|
|
|
|
# Verify current_task visible
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID")
|
|
check "Current task visible" '"current_task":"Running tests"' "$R"
|
|
|
|
# Heartbeat with error rate (trigger degraded — needs >0.5 AND registered)
|
|
R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $DEV_TOKEN" \
|
|
-d "{\"workspace_id\":\"$DEV_ID\",\"error_rate\":0.8,\"sample_error\":\"timeout\"}")
|
|
check "Degraded heartbeat" '"status":"ok"' "$R"
|
|
|
|
# Verify degraded status
|
|
sleep 1
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID")
|
|
check "Dev degraded" '"last_error_rate":0.8' "$R"
|
|
|
|
# Recover
|
|
R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $DEV_TOKEN" \
|
|
-d "{\"workspace_id\":\"$DEV_ID\",\"error_rate\":0.0}")
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID")
|
|
check "Dev recovered" '"last_error_rate":0' "$R"
|
|
|
|
# ============================================================
|
|
# Section 4: Discovery & Access Control
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 4: Discovery & Access Control ---"
|
|
|
|
# PM was registered in Section 2 right after creation.
|
|
|
|
# Discover requires X-Workspace-ID
|
|
CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/registry/discover/$DEV_ID")
|
|
check_status "Discover without header → 400" "400" "$CODE"
|
|
|
|
# PM discovers Dev (parent→child: allowed)
|
|
R=$(curl -s -H "X-Workspace-ID: $PM_ID" -H "Authorization: Bearer $PM_TOKEN" "$BASE/registry/discover/$DEV_ID")
|
|
check "PM discovers Dev (parent→child)" "$DEV_ID" "$R"
|
|
|
|
# Dev discovers QA (siblings: allowed) — QA was registered in Section 2
|
|
R=$(curl -s -H "X-Workspace-ID: $DEV_ID" -H "Authorization: Bearer $DEV_TOKEN" "$BASE/registry/discover/$QA_ID")
|
|
check "Dev discovers QA (siblings)" "$QA_ID" "$R"
|
|
|
|
# Check access: PM → Dev (allowed)
|
|
R=$(curl -s -X POST "$BASE/registry/check-access" -H "Content-Type: application/json" \
|
|
-d "{\"caller_id\":\"$PM_ID\",\"target_id\":\"$DEV_ID\"}")
|
|
check "Access PM→Dev (parent→child)" '"allowed":true' "$R"
|
|
|
|
# Check access: Dev → Outsider (denied)
|
|
R=$(curl -s -X POST "$BASE/registry/check-access" -H "Content-Type: application/json" \
|
|
-d "{\"caller_id\":\"$DEV_ID\",\"target_id\":\"$OUTSIDER_ID\"}")
|
|
check "Access Dev→Outsider (denied)" '"allowed":false' "$R"
|
|
|
|
# Peers — Dev should see PM and QA
|
|
R=$(curl -s -H "X-Workspace-ID: $DEV_ID" -H "Authorization: Bearer $DEV_TOKEN" "$BASE/registry/$DEV_ID/peers")
|
|
check "Dev peers include PM" "$PM_ID" "$R"
|
|
check "Dev peers include QA" "$QA_ID" "$R"
|
|
|
|
# ============================================================
|
|
# Section 5: Secrets
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 5: Secrets ---"
|
|
|
|
# List secrets (initial state — may have secrets from org import .env)
|
|
R=$(curl -s "$BASE/workspaces/$PM_ID/secrets")
|
|
check "List secrets (responds)" '[' "$R"
|
|
|
|
# Set a secret
|
|
R=$(curl -s -X POST "$BASE/workspaces/$PM_ID/secrets" -H "Content-Type: application/json" \
|
|
-d '{"key":"OPENAI_API_KEY","value":"sk-test-12345"}')
|
|
check "Set secret" '"status":"saved"' "$R"
|
|
|
|
# List secrets (1 item, value not exposed)
|
|
R=$(curl -s "$BASE/workspaces/$PM_ID/secrets")
|
|
check "Secret listed" '"key":"OPENAI_API_KEY"' "$R"
|
|
check "Secret value hidden" '"has_value":true' "$R"
|
|
|
|
# Get model (derived from secrets or config)
|
|
R=$(curl -s "$BASE/workspaces/$PM_ID/model")
|
|
# Model endpoint returns whatever is configured
|
|
check "Get model endpoint" '{' "$R"
|
|
|
|
# Delete secret
|
|
R=$(curl -s -X DELETE "$BASE/workspaces/$PM_ID/secrets/OPENAI_API_KEY")
|
|
check "Delete secret" '"status":"deleted"' "$R"
|
|
|
|
# ============================================================
|
|
# Section 6: Config & Files
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 6: Config & Files ---"
|
|
|
|
# Note: Config read requires container or template — test error case
|
|
CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/workspaces/$PM_ID/files/config.yaml")
|
|
# May return 404 if no container/template exists (expected)
|
|
echo " INFO: GET config.yaml → HTTP $CODE (expected 200 or 404)"
|
|
|
|
# ============================================================
|
|
# Section 7: Workspace Memory (HMA)
|
|
# ============================================================
|
|
echo ""
|
|
# Pause before memory tests to avoid rate limits from prior sections
|
|
sleep 3
|
|
echo "--- Section 7: Workspace Memory ---"
|
|
|
|
# Commit LOCAL memory
|
|
R=$(curl -s -X POST "$BASE/workspaces/$DEV_ID/memories" -H "Content-Type: application/json" \
|
|
-d '{"content":"Architecture uses Go + React","scope":"LOCAL"}')
|
|
check "Commit LOCAL memory" '"scope":"LOCAL"' "$R"
|
|
MEM_ID=$(echo "$R" | jq_extract "['id']" 2>/dev/null || echo "")
|
|
|
|
# Commit TEAM memory
|
|
R=$(curl -s -X POST "$BASE/workspaces/$DEV_ID/memories" -H "Content-Type: application/json" \
|
|
-d '{"content":"Sprint goal: ship v2.0 by Friday","scope":"TEAM"}')
|
|
check "Commit TEAM memory" '"scope":"TEAM"' "$R"
|
|
TEAM_MEM_ID=$(echo "$R" | jq_extract "['id']" 2>/dev/null || echo "")
|
|
|
|
# List all memories
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID/memories")
|
|
check "List all memories" 'Architecture uses Go' "$R"
|
|
check "List includes TEAM memory" 'Sprint goal' "$R"
|
|
|
|
# Filter by scope
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID/memories?scope=LOCAL")
|
|
check "Filter LOCAL scope" 'Architecture' "$R"
|
|
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID/memories?scope=TEAM")
|
|
check "Filter TEAM scope" 'Sprint goal' "$R"
|
|
|
|
# Invalid scope rejected
|
|
R=$(curl -s -X POST "$BASE/workspaces/$DEV_ID/memories" -H "Content-Type: application/json" \
|
|
-d '{"content":"test","scope":"INVALID"}')
|
|
check "Invalid scope rejected" 'error' "$R"
|
|
|
|
# Empty content rejected
|
|
R=$(curl -s -X POST "$BASE/workspaces/$DEV_ID/memories" -H "Content-Type: application/json" \
|
|
-d '{"content":"","scope":"LOCAL"}')
|
|
check "Empty content rejected" 'error' "$R"
|
|
|
|
# Memory persists across API calls (simulate recall after restart)
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID/memories")
|
|
check "Memory persists (recall)" 'Architecture' "$R"
|
|
|
|
# Delete memory
|
|
if [ -n "$MEM_ID" ]; then
|
|
R=$(curl -s -X DELETE "$BASE/workspaces/$DEV_ID/memories/$MEM_ID")
|
|
check "Delete LOCAL memory" '"status"' "$R"
|
|
fi
|
|
|
|
# Verify deleted memory is gone
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID/memories?scope=LOCAL")
|
|
if echo "$R" | grep -qF "Architecture"; then
|
|
echo " FAIL: Deleted memory still visible"
|
|
FAIL=$((FAIL + 1))
|
|
else
|
|
echo " PASS: Deleted memory removed"
|
|
PASS=$((PASS + 1))
|
|
fi
|
|
|
|
# Clean up TEAM memory
|
|
if [ -n "$TEAM_MEM_ID" ]; then
|
|
curl -s -X DELETE "$BASE/workspaces/$DEV_ID/memories/$TEAM_MEM_ID" > /dev/null
|
|
fi
|
|
|
|
sleep 2
|
|
# Cross-workspace memory isolation — PM should NOT see Dev's LOCAL memories
|
|
R=$(curl -s -X POST "$BASE/workspaces/$DEV_ID/memories" -H "Content-Type: application/json" \
|
|
-d '{"content":"Dev secret note","scope":"LOCAL"}')
|
|
DEV_SECRET_ID=$(echo "$R" | jq_extract "['id']" 2>/dev/null || echo "")
|
|
|
|
R=$(curl -s "$BASE/workspaces/$PM_ID/memories")
|
|
if echo "$R" | grep -qF "Dev secret note"; then
|
|
echo " FAIL: PM can see Dev's LOCAL memory (isolation broken)"
|
|
FAIL=$((FAIL + 1))
|
|
else
|
|
echo " PASS: Memory isolation — PM cannot see Dev's LOCAL"
|
|
PASS=$((PASS + 1))
|
|
fi
|
|
|
|
# Clean up
|
|
if [ -n "$DEV_SECRET_ID" ]; then
|
|
curl -s -X DELETE "$BASE/workspaces/$DEV_ID/memories/$DEV_SECRET_ID" > /dev/null
|
|
fi
|
|
|
|
# ============================================================
|
|
# Section 8: Activity Logging
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 8: Activity Logging ---"
|
|
|
|
# Report activity
|
|
R=$(curl -s -X POST "$BASE/workspaces/$DEV_ID/activity" -H "Content-Type: application/json" \
|
|
-d '{"activity_type":"agent_log","summary":"Running unit tests","status":"ok"}')
|
|
check "Report activity" '"status"' "$R"
|
|
|
|
# List activity
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID/activity?limit=5")
|
|
check "List activity" 'Running unit tests' "$R"
|
|
|
|
# Filter by type
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID/activity?type=agent_log")
|
|
check "Filter activity by type" 'agent_log' "$R"
|
|
|
|
# ============================================================
|
|
# Section 9: Events
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 9: Events ---"
|
|
|
|
# List global events
|
|
R=$(curl -s "$BASE/events")
|
|
check "List global events" 'WORKSPACE_' "$R"
|
|
|
|
# List events for PM
|
|
R=$(curl -s "$BASE/events/$PM_ID")
|
|
check "List PM events" "$PM_ID" "$R"
|
|
|
|
# ============================================================
|
|
# Section 10: Approvals
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 10: Approvals ---"
|
|
|
|
# Create approval request
|
|
R=$(curl -s -X POST "$BASE/workspaces/$DEV_ID/approvals" -H "Content-Type: application/json" \
|
|
-d '{"action":"deploy to production","reason":"All tests passing"}')
|
|
check "Create approval" '"status":"pending"' "$R"
|
|
APPROVAL_ID=$(echo "$R" | jq_extract "['id']" 2>/dev/null || echo "")
|
|
|
|
# List pending approvals
|
|
R=$(curl -s "$BASE/approvals/pending")
|
|
check "List pending approvals" 'deploy to production' "$R"
|
|
|
|
# List workspace approvals
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID/approvals")
|
|
check "List Dev approvals" 'deploy to production' "$R"
|
|
|
|
# Decide approval
|
|
if [ -n "$APPROVAL_ID" ]; then
|
|
R=$(curl -s -X POST "$BASE/workspaces/$DEV_ID/approvals/$APPROVAL_ID/decide" \
|
|
-H "Content-Type: application/json" -d '{"approved":true,"decided_by":"admin"}')
|
|
check "Approve request" '"approved":true' "$R"
|
|
fi
|
|
|
|
# ============================================================
|
|
# Section 11: Canvas Viewport
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 11: Canvas Viewport ---"
|
|
|
|
R=$(curl -s -X PUT "$BASE/canvas/viewport" -H "Content-Type: application/json" \
|
|
-d '{"x":50,"y":100,"zoom":1.5}')
|
|
check "Save viewport" '"status":"saved"' "$R"
|
|
|
|
R=$(curl -s "$BASE/canvas/viewport")
|
|
check "Get viewport" '"zoom":1.5' "$R"
|
|
|
|
# ============================================================
|
|
# Section 12: Agent Card Update
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 12: Agent Card Update ---"
|
|
|
|
R=$(curl -s -X POST "$BASE/registry/update-card" -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $DEV_TOKEN" \
|
|
-d "{\"workspace_id\":\"$DEV_ID\",\"agent_card\":{\"name\":\"Dev Agent v2\",\"skills\":[{\"id\":\"code\",\"name\":\"Coding\"}],\"version\":\"2.0.0\"}}")
|
|
check "Update agent card" '"status":"updated"' "$R"
|
|
|
|
R=$(curl -s "$BASE/workspaces/$DEV_ID")
|
|
check "Agent card updated" '"name":"Dev Agent v2"' "$R"
|
|
|
|
# ============================================================
|
|
# Section 13: Bundle Export/Import
|
|
# ============================================================
|
|
echo ""
|
|
sleep 3
|
|
echo "--- Section 13: Bundle Export/Import ---"
|
|
|
|
# Export PM bundle
|
|
R=$(curl -s "$BASE/bundles/export/$PM_ID")
|
|
check "Export PM bundle" '"name":"Test PM"' "$R"
|
|
check "Bundle has workspace data" '"name":"Test PM"' "$R"
|
|
|
|
# Import bundle (create from exported data)
|
|
BUNDLE=$(curl -s "$BASE/bundles/export/$PM_ID")
|
|
R=$(curl -s -X POST "$BASE/bundles/import" -H "Content-Type: application/json" -d "$BUNDLE")
|
|
check "Import bundle" '"status"' "$R"
|
|
|
|
# ============================================================
|
|
# Section 14: Workspace Delete (Cascade)
|
|
# ============================================================
|
|
echo ""
|
|
echo "--- Section 14: Cleanup & Delete ---"
|
|
|
|
# Delete with children — should require confirmation
|
|
R=$(curl -s -X DELETE "$BASE/workspaces/$PM_ID")
|
|
check "Delete PM requires confirmation" '"confirmation_required"' "$R"
|
|
|
|
# Delete with confirmation
|
|
R=$(curl -s -X DELETE "$BASE/workspaces/$PM_ID?confirm=true")
|
|
check "Delete PM cascades" '"cascade_deleted"' "$R"
|
|
|
|
# Delete outsider
|
|
curl -s -X DELETE "$BASE/workspaces/$OUTSIDER_ID?confirm=true" > /dev/null
|
|
|
|
# Clean up remaining workspaces (bundle imports, runtime test workspaces, etc.)
|
|
sleep 2
|
|
curl -s "$BASE/workspaces" | python3 -c "
|
|
import json, sys, subprocess, time
|
|
ws = json.load(sys.stdin)
|
|
for w in ws:
|
|
time.sleep(0.5) # avoid rate limit
|
|
subprocess.run(['curl', '-s', '-X', 'DELETE', '$BASE/workspaces/' + w['id'] + '?confirm=true'], capture_output=True)
|
|
" 2>/dev/null
|
|
|
|
# Poll for clean state up to 30s — DB cascade + container stop is async on busy systems
|
|
for _ in 1 2 3 4 5 6; do
|
|
sleep 5
|
|
R=$(curl -s "$BASE/workspaces")
|
|
if [ "$R" = "[]" ]; then break; fi
|
|
done
|
|
check "All workspaces cleaned" '[]' "$R"
|
|
|
|
# ============================================================
|
|
# Summary
|
|
# ============================================================
|
|
echo ""
|
|
echo "============================================"
|
|
echo " Results: $PASS passed, $FAIL failed, $SKIP skipped"
|
|
echo " Total: $((PASS + FAIL + SKIP)) checks"
|
|
echo "============================================"
|
|
|
|
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|