fix: stress test setup process and report structure workflow for Dify 1.15.0+ (#38194)

This commit is contained in:
Blackoutta 2026-06-30 15:29:42 +08:00 committed by GitHub
parent 4303103304
commit b4be4d90a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 444 additions and 117 deletions

View File

@ -84,10 +84,10 @@ The stress test tests a single endpoint with comprehensive SSE metrics tracking:
## Prerequisites ## Prerequisites
1. **Dependencies are automatically installed** when running setup: 1. **Dependencies**:
- Locust (load testing framework) - Locust runs through `uvx --from locust`, outside the API project environment.
- sseclient-py (SSE client library) - `sseclient-py` is included in the API project dependencies.
1. **Complete Dify setup**: 1. **Complete Dify setup**:
@ -96,6 +96,25 @@ The stress test tests a single endpoint with comprehensive SSE metrics tracking:
python scripts/stress-test/setup_all.py python scripts/stress-test/setup_all.py
``` ```
For a brand-new Dify instance, the setup script creates the first admin account. Override the defaults if needed:
```bash
STRESS_TEST_ADMIN_EMAIL='your-admin@example.com' \
STRESS_TEST_ADMIN_USERNAME='dify' \
STRESS_TEST_ADMIN_PASSWORD='your-password' \
python scripts/stress-test/setup_all.py
```
For an already-initialized Dify instance with an admin account, provide the existing admin login:
```bash
STRESS_TEST_ADMIN_EMAIL='your-admin@example.com' \
STRESS_TEST_ADMIN_PASSWORD='your-password' \
python scripts/stress-test/setup_all.py
```
`STRESS_TEST_ADMIN_USERNAME` is only used in the brand-new instance case, when `/console/api/setup` creates the first admin account.
1. **Ensure services are running**: 1. **Ensure services are running**:
**IMPORTANT**: For accurate stress testing, run the API server with Gunicorn in production mode: **IMPORTANT**: For accurate stress testing, run the API server with Gunicorn in production mode:
@ -141,11 +160,11 @@ The stress test tests a single endpoint with comprehensive SSE metrics tracking:
# Run with default configuration (headless mode) # Run with default configuration (headless mode)
./scripts/stress-test/run_locust_stress_test.sh ./scripts/stress-test/run_locust_stress_test.sh
# Or run directly with uv # Or run directly with uvx
uv run --project api python -m locust -f scripts/stress-test/sse_benchmark.py --host http://localhost:5001 uvx --from locust locust -f scripts/stress-test/sse_benchmark.py --host http://localhost:5001
# Run with Web UI (access at http://localhost:8089) # Run with Web UI (access at http://localhost:8089)
uv run --project api python -m locust -f scripts/stress-test/sse_benchmark.py --host http://localhost:5001 --web-port 8089 uvx --from locust locust -f scripts/stress-test/sse_benchmark.py --host http://localhost:5001 --web-port 8089
``` ```
The script will: The script will:
@ -182,12 +201,13 @@ self.questions = [
### Report Structure ### Report Structure
After running the stress test, you'll find these files in the `reports/` directory: After running the stress test, you'll find one directory per run under `reports/`:
- `locust_summary_YYYYMMDD_HHMMSS.txt` - Complete console output with metrics - `YYYYMMDD_HHMMSS/locust_summary.txt` - Complete console output with metrics
- `locust_report_YYYYMMDD_HHMMSS.html` - Interactive HTML report with charts - `YYYYMMDD_HHMMSS/locust_report.html` - Interactive HTML report with charts
- `locust_YYYYMMDD_HHMMSS_stats.csv` - CSV with detailed statistics - `YYYYMMDD_HHMMSS/locust_stats.csv` - CSV with detailed statistics
- `locust_YYYYMMDD_HHMMSS_stats_history.csv` - Time-series data - `YYYYMMDD_HHMMSS/locust_stats_history.csv` - Time-series data
- `YYYYMMDD_HHMMSS/sse_metrics_YYYYMMDD_HHMMSS.json` - Custom SSE metrics
### Key Metrics ### Key Metrics
@ -399,8 +419,8 @@ docker compose -f docker/docker-compose.middleware.yaml up -d db
1. **"ModuleNotFoundError: No module named 'locust'"**: 1. **"ModuleNotFoundError: No module named 'locust'"**:
```bash ```bash
# Dependencies are installed automatically, but if needed: # Locust is intentionally run outside the api project environment:
uv --project api add --dev locust sseclient-py uvx --from locust locust --version
``` ```
1. **"API key configuration not found"**: 1. **"API key configuration not found"**:
@ -453,15 +473,15 @@ Run Locust directly with custom options:
```bash ```bash
# With specific user count and spawn rate # With specific user count and spawn rate
uv run --project api python -m locust -f scripts/stress-test/sse_benchmark.py \ uvx --from locust locust -f scripts/stress-test/sse_benchmark.py \
--host http://localhost:5001 --users 50 --spawn-rate 5 --host http://localhost:5001 --users 50 --spawn-rate 5
# Generate CSV reports # Generate CSV reports
uv run --project api python -m locust -f scripts/stress-test/sse_benchmark.py \ uvx --from locust locust -f scripts/stress-test/sse_benchmark.py \
--host http://localhost:5001 --csv reports/results --host http://localhost:5001 --csv reports/results
# Run for specific duration # Run for specific duration
uv run --project api python -m locust -f scripts/stress-test/sse_benchmark.py \ uvx --from locust locust -f scripts/stress-test/sse_benchmark.py \
--host http://localhost:5001 --run-time 5m --headless --host http://localhost:5001 --run-time 5m --headless
``` ```
@ -469,7 +489,7 @@ uv run --project api python -m locust -f scripts/stress-test/sse_benchmark.py \
```bash ```bash
# Compare multiple stress test runs # Compare multiple stress test runs
ls -la reports/stress_test_*.txt | tail -5 ls -la scripts/stress-test/reports/*/locust_summary.txt | tail -5
``` ```
## Interpreting Performance Issues ## Interpreting Performance Issues

View File

@ -8,9 +8,9 @@ from typing import NotRequired, TypedDict
class AdminConfig(TypedDict): class AdminConfig(TypedDict):
"""Configuration for admin section.""" """Configuration for admin section."""
email: str
username: str username: str
password: str password: str
base_url: str
class AuthConfig(TypedDict): class AuthConfig(TypedDict):
@ -18,6 +18,7 @@ class AuthConfig(TypedDict):
access_token: str access_token: str
refresh_token: NotRequired[str] refresh_token: NotRequired[str]
csrf_token: NotRequired[str]
expires_at: NotRequired[int] expires_at: NotRequired[int]
@ -253,18 +254,25 @@ class ConfigHelper:
Returns: Returns:
Access token string or None if not found Access token string or None if not found
""" """
auth = self.get_state_section[AuthConfig]("auth") auth = self.get_state_section("auth")
if auth: if auth:
return auth.get("access_token") return auth.get("access_token")
return None return None
def get_csrf_token(self) -> str | None:
"""Get the CSRF token from auth section."""
auth = self.get_state_section("auth")
if auth:
return auth.get("csrf_token")
return None
def get_app_id(self) -> str | None: def get_app_id(self) -> str | None:
"""Get the app ID from app section. """Get the app ID from app section.
Returns: Returns:
App ID string or None if not found App ID string or None if not found
""" """
app = self.get_state_section[AppConfig]("app") app = self.get_state_section("app")
if app: if app:
return app.get("app_id") return app.get("app_id")
return None return None
@ -275,11 +283,31 @@ class ConfigHelper:
Returns: Returns:
API key token string or None if not found API key token string or None if not found
""" """
api_key = self.get_state_section[ApiKeyConfig]("api_key") api_key = self.get_state_section("api_key")
if api_key: if api_key:
return api_key.get("token") return api_key.get("token")
return None return None
def console_auth_headers(self) -> dict[str, str]:
access_token = self.get_token()
csrf_token = self.get_csrf_token()
headers: dict[str, str] = {}
if access_token:
headers["authorization"] = f"Bearer {access_token}"
if csrf_token:
headers["X-CSRF-Token"] = csrf_token
return headers
def console_auth_cookies(self) -> dict[str, str]:
access_token = self.get_token()
csrf_token = self.get_csrf_token()
cookies = {"locale": "en-US"}
if access_token:
cookies["access_token"] = access_token
if csrf_token:
cookies["csrf_token"] = csrf_token
return cookies
# Create a default instance for convenience # Create a default instance for convenience
config_helper = ConfigHelper() config_helper = ConfigHelper()

View File

@ -21,10 +21,12 @@ NC='\033[0m' # No Color
# Configuration # Configuration
TIMESTAMP=$(date +"%Y%m%d_%H%M%S") TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
REPORT_DIR="${STRESS_TEST_DIR}/reports" START_EPOCH=$(date +%s)
CSV_PREFIX="${REPORT_DIR}/locust_${TIMESTAMP}" REPORT_ROOT="${STRESS_TEST_DIR}/reports"
HTML_REPORT="${REPORT_DIR}/locust_report_${TIMESTAMP}.html" REPORT_DIR="${REPORT_ROOT}/${TIMESTAMP}"
SUMMARY_REPORT="${REPORT_DIR}/locust_summary_${TIMESTAMP}.txt" CSV_PREFIX="${REPORT_DIR}/locust"
HTML_REPORT="${REPORT_DIR}/locust_report.html"
SUMMARY_REPORT="${REPORT_DIR}/locust_summary.txt"
# Create reports directory if it doesn't exist # Create reports directory if it doesn't exist
mkdir -p "${REPORT_DIR}" mkdir -p "${REPORT_DIR}"
@ -111,6 +113,7 @@ echo
# Use SSE stress test script # Use SSE stress test script
LOCUST_SCRIPT="${STRESS_TEST_DIR}/sse_benchmark.py" LOCUST_SCRIPT="${STRESS_TEST_DIR}/sse_benchmark.py"
LOCUST_RUN=(uvx --from locust locust)
# Prepare Locust command # Prepare Locust command
if [ "$choice" = "2" ]; then if [ "$choice" = "2" ]; then
@ -119,7 +122,7 @@ if [ "$choice" = "2" ]; then
echo echo
# Run with web UI # Run with web UI
uv --project api run locust \ "${LOCUST_RUN[@]}" \
-f ${LOCUST_SCRIPT} \ -f ${LOCUST_SCRIPT} \
--host http://localhost:5001 \ --host http://localhost:5001 \
--web-port 8089 --web-port 8089
@ -128,7 +131,7 @@ else
echo echo
# Run in headless mode with CSV output # Run in headless mode with CSV output
uv --project api run locust \ "${LOCUST_RUN[@]}" \
-f ${LOCUST_SCRIPT} \ -f ${LOCUST_SCRIPT} \
--host http://localhost:5001 \ --host http://localhost:5001 \
--users $USERS \ --users $USERS \
@ -139,6 +142,22 @@ else
--csv=$CSV_PREFIX \ --csv=$CSV_PREFIX \
--html=$HTML_REPORT \ --html=$HTML_REPORT \
2>&1 | tee $SUMMARY_REPORT 2>&1 | tee $SUMMARY_REPORT
SSE_METRICS_REPORT=$(python3 - <<EOF
from pathlib import Path
import shutil
reports = [
p for p in Path("${REPORT_ROOT}").glob("sse_metrics_*.json")
if p.stat().st_mtime >= ${START_EPOCH}
]
if reports:
source = max(reports, key=lambda p: p.stat().st_mtime)
target = Path("${REPORT_DIR}") / source.name
if source != target:
shutil.move(str(source), str(target))
print(target)
EOF
)
echo echo
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}" echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
@ -150,6 +169,11 @@ else
echo -e " ${YELLOW}HTML Report:${NC} $HTML_REPORT" echo -e " ${YELLOW}HTML Report:${NC} $HTML_REPORT"
echo -e " ${YELLOW}CSV Stats:${NC} ${CSV_PREFIX}_stats.csv" echo -e " ${YELLOW}CSV Stats:${NC} ${CSV_PREFIX}_stats.csv"
echo -e " ${YELLOW}CSV History:${NC} ${CSV_PREFIX}_stats_history.csv" echo -e " ${YELLOW}CSV History:${NC} ${CSV_PREFIX}_stats_history.csv"
if [ -n "$SSE_METRICS_REPORT" ]; then
echo -e " ${YELLOW}SSE Metrics:${NC} $SSE_METRICS_REPORT"
else
echo -e " ${YELLOW}SSE Metrics:${NC} not found"
fi
echo echo
echo -e "${CYAN}View HTML report:${NC}" echo -e "${CYAN}View HTML report:${NC}"
echo " open $HTML_REPORT # macOS" echo " open $HTML_REPORT # macOS"
@ -184,14 +208,22 @@ try:
print(f" RPS: {row.get('Requests/s', 'N/A')}") print(f" RPS: {row.get('Requests/s', 'N/A')}")
break break
# Show SSE-specific metrics
print() print()
print("SSE Streaming Metrics:") print("SSE Streaming Metrics:")
for row in rows: import json
if 'Time to First Event' in row.get('Name', ''): sse_metrics_report = "${SSE_METRICS_REPORT}"
print(f" Time to First Event: {row.get('Median Response Time', 'N/A')} ms (median)") if sse_metrics_report:
elif 'Stream Duration' in row.get('Name', ''): with open(sse_metrics_report, 'r') as metrics_file:
print(f" Stream Duration: {row.get('Median Response Time', 'N/A')} ms (median)") metrics = json.load(metrics_file).get("metrics", {})
print(f" Total Connections: {metrics.get('total_connections', 'N/A')}")
print(f" Total Events: {metrics.get('total_events', 'N/A')}")
print(f" Connection Rate: {metrics.get('overall_conn_rate', 0):.2f} conn/s")
print(f" Event Throughput: {metrics.get('overall_event_rate', 0):.2f} events/s")
print(f" TTFE: {metrics.get('ttfe_p50', 0):.1f} ms p50 / {metrics.get('ttfe_p95', 0):.1f} ms p95")
print(f" Stream Duration: {metrics.get('stream_duration_p50', 0):.1f} ms p50 / {metrics.get('stream_duration_p95', 0):.1f} ms p95")
print(f" Inter-event Latency: {metrics.get('inter_event_latency_p50', 0):.1f} ms p50 / {metrics.get('inter_event_latency_p95', 0):.1f} ms p95")
else:
print(" No SSE metrics JSON report found for this run")
except Exception as e: except Exception as e:
print(f"Could not parse metrics: {e}") print(f"Could not parse metrics: {e}")
@ -199,4 +231,4 @@ EOF
fi fi
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
fi fi

View File

@ -9,7 +9,7 @@ import httpx
from common import Logger, config_helper from common import Logger, config_helper
def configure_openai_plugin() -> None: def configure_openai_plugin() -> bool:
"""Configure OpenAI plugin with mock server credentials.""" """Configure OpenAI plugin with mock server credentials."""
log = Logger("ConfigPlugin") log = Logger("ConfigPlugin")
@ -20,7 +20,7 @@ def configure_openai_plugin() -> None:
if not access_token: if not access_token:
log.error("No access token found in config") log.error("No access token found in config")
log.info("Please run login_admin.py first to get access token") log.info("Please run login_admin.py first to get access token")
return return False
log.step("Configuring OpenAI plugin with mock server...") log.step("Configuring OpenAI plugin with mock server...")
@ -50,14 +50,14 @@ def configure_openai_plugin() -> None:
"Sec-Fetch-Mode": "cors", "Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site", "Sec-Fetch-Site": "same-site",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"authorization": f"Bearer {access_token}", **config_helper.console_auth_headers(),
"content-type": "application/json", "content-type": "application/json",
"sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"', "sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
"sec-ch-ua-mobile": "?0", "sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"', "sec-ch-ua-platform": '"macOS"',
} }
cookies = {"locale": "en-US"} cookies = config_helper.console_auth_cookies()
try: try:
# Make the configuration request # Make the configuration request
@ -73,25 +73,32 @@ def configure_openai_plugin() -> None:
log.success("OpenAI plugin configured successfully!") log.success("OpenAI plugin configured successfully!")
log.key_value("API Base", config_payload["credentials"]["openai_api_base"]) log.key_value("API Base", config_payload["credentials"]["openai_api_base"])
log.key_value("API Key", config_payload["credentials"]["openai_api_key"]) log.key_value("API Key", config_payload["credentials"]["openai_api_key"])
return True
elif response.status_code == 201: elif response.status_code == 201:
log.success("OpenAI plugin credentials created successfully!") log.success("OpenAI plugin credentials created successfully!")
log.key_value("API Base", config_payload["credentials"]["openai_api_base"]) log.key_value("API Base", config_payload["credentials"]["openai_api_base"])
log.key_value("API Key", config_payload["credentials"]["openai_api_key"]) log.key_value("API Key", config_payload["credentials"]["openai_api_key"])
return True
elif response.status_code == 401: elif response.status_code == 401:
log.error("Configuration failed: Unauthorized") log.error("Configuration failed: Unauthorized")
log.info("Token may have expired. Please run login_admin.py again") log.info("Token may have expired. Please run login_admin.py again")
return False
else: else:
log.error(f"Configuration failed with status code: {response.status_code}") log.error(f"Configuration failed with status code: {response.status_code}")
log.debug(f"Response: {response.text}") log.debug(f"Response: {response.text}")
return False
except httpx.ConnectError: except httpx.ConnectError:
log.error("Could not connect to Dify API at http://localhost:5001") log.error("Could not connect to Dify API at http://localhost:5001")
log.info("Make sure the API server is running with: ./dev/start-api") log.info("Make sure the API server is running with: ./dev/start-api")
return False
except Exception as e: except Exception as e:
log.error(f"An error occurred: {e}") log.error(f"An error occurred: {e}")
return False
if __name__ == "__main__": if __name__ == "__main__":
configure_openai_plugin() if not configure_openai_plugin():
sys.exit(1)

View File

@ -11,7 +11,7 @@ import httpx
from common import Logger, config_helper from common import Logger, config_helper
def create_api_key() -> None: def create_api_key() -> bool:
"""Create API key for the imported app.""" """Create API key for the imported app."""
log = Logger("CreateAPIKey") log = Logger("CreateAPIKey")
@ -21,14 +21,14 @@ def create_api_key() -> None:
access_token = config_helper.get_token() access_token = config_helper.get_token()
if not access_token: if not access_token:
log.error("No access token found in config") log.error("No access token found in config")
return return False
# Read app_id from config # Read app_id from config
app_id = config_helper.get_app_id() app_id = config_helper.get_app_id()
if not app_id: if not app_id:
log.error("No app_id found in config") log.error("No app_id found in config")
log.info("Please run import_workflow_app.py first to import the app") log.info("Please run import_workflow_app.py first to import the app")
return return False
log.step(f"Creating API key for app: {app_id}") log.step(f"Creating API key for app: {app_id}")
@ -50,14 +50,14 @@ def create_api_key() -> None:
"Sec-Fetch-Mode": "cors", "Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site", "Sec-Fetch-Site": "same-site",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"authorization": f"Bearer {access_token}", **config_helper.console_auth_headers(),
"content-type": "application/json", "content-type": "application/json",
"sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"', "sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
"sec-ch-ua-mobile": "?0", "sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"', "sec-ch-ua-platform": '"macOS"',
} }
cookies = {"locale": "en-US"} cookies = config_helper.console_auth_cookies()
try: try:
# Make the API key creation request # Make the API key creation request
@ -91,23 +91,31 @@ def create_api_key() -> None:
if config_helper.write_config("api_key_config", api_key_config): if config_helper.write_config("api_key_config", api_key_config):
log.info(f"API key saved to: {config_helper.get_config_path('benchmark_state')}") log.info(f"API key saved to: {config_helper.get_config_path('benchmark_state')}")
return True
return False
else: else:
log.error("No API token received") log.error("No API token received")
log.debug(f"Response: {json.dumps(response_data, indent=2)}") log.debug(f"Response: {json.dumps(response_data, indent=2)}")
return False
elif response.status_code == 401: elif response.status_code == 401:
log.error("API key creation failed: Unauthorized") log.error("API key creation failed: Unauthorized")
log.info("Token may have expired. Please run login_admin.py again") log.info("Token may have expired. Please run login_admin.py again")
return False
else: else:
log.error(f"API key creation failed with status code: {response.status_code}") log.error(f"API key creation failed with status code: {response.status_code}")
log.debug(f"Response: {response.text}") log.debug(f"Response: {response.text}")
return False
except httpx.ConnectError: except httpx.ConnectError:
log.error("Could not connect to Dify API at http://localhost:5001") log.error("Could not connect to Dify API at http://localhost:5001")
log.info("Make sure the API server is running with: ./dev/start-api") log.info("Make sure the API server is running with: ./dev/start-api")
return False
except Exception as e: except Exception as e:
log.error(f"An error occurred: {e}") log.error(f"An error occurred: {e}")
return False
if __name__ == "__main__": if __name__ == "__main__":
create_api_key() if not create_api_key():
sys.exit(1)

View File

@ -11,7 +11,11 @@ import httpx
from common import Logger, config_helper # type: ignore[import] from common import Logger, config_helper # type: ignore[import]
def import_workflow_app() -> None: def is_successful_import_response(response_data: dict[str, object]) -> bool:
return response_data.get("status") != "failed" and bool(response_data.get("app_id"))
def import_workflow_app() -> bool:
"""Import workflow app from DSL file and save app_id.""" """Import workflow app from DSL file and save app_id."""
log = Logger("ImportApp") log = Logger("ImportApp")
@ -22,14 +26,14 @@ def import_workflow_app() -> None:
if not access_token: if not access_token:
log.error("No access token found in config") log.error("No access token found in config")
log.info("Please run login_admin.py first to get access token") log.info("Please run login_admin.py first to get access token")
return return False
# Read workflow DSL file # Read workflow DSL file
dsl_path = Path(__file__).parent / "dsl" / "workflow_llm.yml" dsl_path = Path(__file__).parent / "dsl" / "workflow_llm.yml"
if not dsl_path.exists(): if not dsl_path.exists():
log.error(f"DSL file not found: {dsl_path}") log.error(f"DSL file not found: {dsl_path}")
return return False
with open(dsl_path) as f: with open(dsl_path) as f:
yaml_content = f.read() yaml_content = f.read()
@ -57,14 +61,14 @@ def import_workflow_app() -> None:
"Sec-Fetch-Mode": "cors", "Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site", "Sec-Fetch-Site": "same-site",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"authorization": f"Bearer {access_token}", **config_helper.console_auth_headers(),
"content-type": "application/json", "content-type": "application/json",
"sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"', "sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
"sec-ch-ua-mobile": "?0", "sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"', "sec-ch-ua-platform": '"macOS"',
} }
cookies = {"locale": "en-US"} cookies = config_helper.console_auth_cookies()
try: try:
# Make the import request # Make the import request
@ -79,50 +83,56 @@ def import_workflow_app() -> None:
if response.status_code == 200: if response.status_code == 200:
response_data = response.json() response_data = response.json()
# Check import status if is_successful_import_response(response_data):
if response_data.get("status") == "completed":
app_id = response_data.get("app_id") app_id = response_data.get("app_id")
if response_data.get("status") != "completed":
if app_id: log.warning(f"Import status: {response_data.get('status')}")
log.success("Workflow app imported successfully!")
log.key_value("App ID", app_id)
log.key_value("App Mode", response_data.get("app_mode"))
log.key_value("DSL Version", response_data.get("imported_dsl_version"))
# Save app_id to config
app_config = {
"app_id": app_id,
"app_mode": response_data.get("app_mode"),
"app_name": "workflow_llm",
"dsl_version": response_data.get("imported_dsl_version"),
}
if config_helper.write_config("app_config", app_config):
log.info(f"App config saved to: {config_helper.get_config_path('benchmark_state')}")
else:
log.error("Import completed but no app_id received")
log.debug(f"Response: {json.dumps(response_data, indent=2)}") log.debug(f"Response: {json.dumps(response_data, indent=2)}")
log.success("Workflow app imported successfully!")
log.key_value("App ID", app_id)
log.key_value("App Mode", response_data.get("app_mode"))
log.key_value("DSL Version", response_data.get("imported_dsl_version"))
# Save app_id to config
app_config = {
"app_id": app_id,
"app_mode": response_data.get("app_mode"),
"app_name": "workflow_llm",
"dsl_version": response_data.get("imported_dsl_version"),
}
if config_helper.write_config("app_config", app_config):
log.info(f"App config saved to: {config_helper.get_config_path('benchmark_state')}")
return True
return False
elif response_data.get("status") == "failed": elif response_data.get("status") == "failed":
log.error("Import failed") log.error("Import failed")
log.error(f"Error: {response_data.get('error')}") log.error(f"Error: {response_data.get('error')}")
return False
else: else:
log.warning(f"Import status: {response_data.get('status')}") log.error("Import response did not include app_id")
log.debug(f"Response: {json.dumps(response_data, indent=2)}") log.debug(f"Response: {json.dumps(response_data, indent=2)}")
return False
elif response.status_code == 401: elif response.status_code == 401:
log.error("Import failed: Unauthorized") log.error("Import failed: Unauthorized")
log.info("Token may have expired. Please run login_admin.py again") log.info("Token may have expired. Please run login_admin.py again")
return False
else: else:
log.error(f"Import failed with status code: {response.status_code}") log.error(f"Import failed with status code: {response.status_code}")
log.debug(f"Response: {response.text}") log.debug(f"Response: {response.text}")
return False
except httpx.ConnectError: except httpx.ConnectError:
log.error("Could not connect to Dify API at http://localhost:5001") log.error("Could not connect to Dify API at http://localhost:5001")
log.info("Make sure the API server is running with: ./dev/start-api") log.info("Make sure the API server is running with: ./dev/start-api")
return False
except Exception as e: except Exception as e:
log.error(f"An error occurred: {e}") log.error(f"An error occurred: {e}")
return False
if __name__ == "__main__": if __name__ == "__main__":
import_workflow_app() if not import_workflow_app():
sys.exit(1)

View File

@ -11,7 +11,11 @@ import httpx
from common import Logger, config_helper from common import Logger, config_helper
def install_openai_plugin() -> None: def is_non_blocking_install_response(response_data: dict[str, object]) -> bool:
return not response_data.get("code")
def install_openai_plugin() -> bool:
"""Install OpenAI plugin using saved access token.""" """Install OpenAI plugin using saved access token."""
log = Logger("InstallPlugin") log = Logger("InstallPlugin")
@ -22,7 +26,7 @@ def install_openai_plugin() -> None:
if not access_token: if not access_token:
log.error("No access token found in config") log.error("No access token found in config")
log.info("Please run login_admin.py first to get access token") log.info("Please run login_admin.py first to get access token")
return return False
log.step("Installing OpenAI plugin...") log.step("Installing OpenAI plugin...")
@ -50,14 +54,14 @@ def install_openai_plugin() -> None:
"Sec-Fetch-Mode": "cors", "Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site", "Sec-Fetch-Site": "same-site",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"authorization": f"Bearer {access_token}", **config_helper.console_auth_headers(),
"content-type": "application/json", "content-type": "application/json",
"sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"', "sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
"sec-ch-ua-mobile": "?0", "sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"', "sec-ch-ua-platform": '"macOS"',
} }
cookies = {"locale": "en-US"} cookies = config_helper.console_auth_cookies()
try: try:
# Make the installation request # Make the installation request
@ -74,8 +78,13 @@ def install_openai_plugin() -> None:
task_id = response_data.get("task_id") task_id = response_data.get("task_id")
if not task_id: if not task_id:
if is_non_blocking_install_response(response_data):
log.warning("No installation task returned; plugin may already be installed")
log.debug(f"Response: {response.text}")
return True
log.error("No task ID received from installation request") log.error("No task ID received from installation request")
return log.debug(f"Response: {response.text}")
return False
log.progress(f"Installation task created: {task_id}") log.progress(f"Installation task created: {task_id}")
log.info("Polling for task completion...") log.info("Polling for task completion...")
@ -103,7 +112,7 @@ def install_openai_plugin() -> None:
success=False, success=False,
message=f"Failed to get task status: {task_response.status_code}", message=f"Failed to get task status: {task_response.status_code}",
) )
return return False
task_data = task_response.json() task_data = task_response.json()
task_info = task_data.get("task", {}) task_info = task_data.get("task", {})
@ -119,7 +128,7 @@ def install_openai_plugin() -> None:
plugin_info = plugins[0] plugin_info = plugins[0]
log.key_value("Plugin ID", plugin_info.get("plugin_id")) log.key_value("Plugin ID", plugin_info.get("plugin_id"))
log.key_value("Message", plugin_info.get("message")) log.key_value("Message", plugin_info.get("message"))
break return True
elif status == "failed": elif status == "failed":
log.spinner_stop(success=False, message="Installation failed") log.spinner_stop(success=False, message="Installation failed")
@ -128,30 +137,37 @@ def install_openai_plugin() -> None:
if plugins: if plugins:
for plugin in plugins: for plugin in plugins:
log.list_item(f"{plugin.get('plugin_id')}: {plugin.get('message')}") log.list_item(f"{plugin.get('plugin_id')}: {plugin.get('message')}")
break return False
# Continue polling if status is "pending" or other # Continue polling if status is "pending" or other
else: else:
log.spinner_stop(success=False, message="Installation timed out") log.spinner_stop(success=False, message="Installation timed out")
log.error("Installation timed out after 60 seconds") log.error("Installation timed out after 60 seconds")
return False
elif response.status_code == 401: elif response.status_code == 401:
log.error("Installation failed: Unauthorized") log.error("Installation failed: Unauthorized")
log.info("Token may have expired. Please run login_admin.py again") log.info("Token may have expired. Please run login_admin.py again")
return False
elif response.status_code == 409: elif response.status_code == 409:
log.warning("Plugin may already be installed") log.warning("Plugin may already be installed")
log.debug(f"Response: {response.text}") log.debug(f"Response: {response.text}")
return True
else: else:
log.error(f"Installation failed with status code: {response.status_code}") log.error(f"Installation failed with status code: {response.status_code}")
log.debug(f"Response: {response.text}") log.debug(f"Response: {response.text}")
return False
except httpx.ConnectError: except httpx.ConnectError:
log.error("Could not connect to Dify API at http://localhost:5001") log.error("Could not connect to Dify API at http://localhost:5001")
log.info("Make sure the API server is running with: ./dev/start-api") log.info("Make sure the API server is running with: ./dev/start-api")
return False
except Exception as e: except Exception as e:
log.error(f"An error occurred: {e}") log.error(f"An error occurred: {e}")
return False
if __name__ == "__main__": if __name__ == "__main__":
install_openai_plugin() if not install_openai_plugin():
sys.exit(1)

View File

@ -5,13 +5,19 @@ from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent)) sys.path.append(str(Path(__file__).parent.parent))
import base64
import json import json
import httpx import httpx
from common import Logger, config_helper from common import Logger, config_helper
def login_admin() -> None: def encode_sensitive_field(value: str) -> str:
"""Encode fields the same way the web client does before login."""
return base64.b64encode(value.encode("utf-8")).decode()
def login_admin() -> bool:
"""Login with admin account and save access token.""" """Login with admin account and save access token."""
log = Logger("Login") log = Logger("Login")
@ -23,7 +29,7 @@ def login_admin() -> None:
if not admin_config: if not admin_config:
log.error("Admin config not found") log.error("Admin config not found")
log.info("Please run setup_admin.py first to create the admin account") log.info("Please run setup_admin.py first to create the admin account")
return return False
log.info(f"Logging in with email: {admin_config['email']}") log.info(f"Logging in with email: {admin_config['email']}")
@ -34,7 +40,7 @@ def login_admin() -> None:
# Prepare login payload # Prepare login payload
login_payload = { login_payload = {
"email": admin_config["email"], "email": admin_config["email"],
"password": admin_config["password"], "password": encode_sensitive_field(admin_config["password"]),
"remember_me": True, "remember_me": True,
} }
@ -50,29 +56,28 @@ def login_admin() -> None:
if response.status_code == 200: if response.status_code == 200:
log.success("Login successful!") log.success("Login successful!")
# Extract token from response
response_data = response.json() response_data = response.json()
# Check if login was successful # Check if login was successful
if response_data.get("result") != "success": if response_data.get("result") != "success":
log.error(f"Login failed: {response_data}") log.error(f"Login failed: {response_data}")
return return False
# Extract tokens from data field access_token = response.cookies.get("access_token", "")
token_data = response_data.get("data", {}) refresh_token = response.cookies.get("refresh_token", "")
access_token = token_data.get("access_token", "") csrf_token = response.cookies.get("csrf_token", "")
refresh_token = token_data.get("refresh_token", "")
if not access_token: if not access_token:
log.error("No access token found in response") log.error("No access token found in response")
log.debug(f"Full response: {json.dumps(response_data, indent=2)}") log.debug(f"Full response: {json.dumps(response_data, indent=2)}")
return return False
# Save token to config file # Save token to config file
token_config = { token_config = {
"email": admin_config["email"], "email": admin_config["email"],
"access_token": access_token, "access_token": access_token,
"refresh_token": refresh_token, "refresh_token": refresh_token,
"csrf_token": csrf_token,
} }
# Save token config # Save token config
@ -82,20 +87,26 @@ def login_admin() -> None:
# Show truncated token for verification # Show truncated token for verification
token_display = f"{access_token[:20]}..." if len(access_token) > 20 else "Token saved" token_display = f"{access_token[:20]}..." if len(access_token) > 20 else "Token saved"
log.key_value("Access token", token_display) log.key_value("Access token", token_display)
return True
elif response.status_code == 401: elif response.status_code == 401:
log.error("Login failed: Invalid credentials") log.error("Login failed: Invalid credentials")
log.debug(f"Response: {response.text}") log.debug(f"Response: {response.text}")
return False
else: else:
log.error(f"Login failed with status code: {response.status_code}") log.error(f"Login failed with status code: {response.status_code}")
log.debug(f"Response: {response.text}") log.debug(f"Response: {response.text}")
return False
except httpx.ConnectError: except httpx.ConnectError:
log.error("Could not connect to Dify API at http://localhost:5001") log.error("Could not connect to Dify API at http://localhost:5001")
log.info("Make sure the API server is running with: ./dev/start-api") log.info("Make sure the API server is running with: ./dev/start-api")
return False
except Exception as e: except Exception as e:
log.error(f"An error occurred: {e}") log.error(f"An error occurred: {e}")
return False
if __name__ == "__main__": if __name__ == "__main__":
login_admin() if not login_admin():
sys.exit(1)

View File

@ -11,7 +11,7 @@ import httpx
from common import Logger, config_helper from common import Logger, config_helper
def publish_workflow() -> None: def publish_workflow() -> bool:
"""Publish the imported workflow app.""" """Publish the imported workflow app."""
log = Logger("PublishWorkflow") log = Logger("PublishWorkflow")
@ -21,13 +21,13 @@ def publish_workflow() -> None:
access_token = config_helper.get_token() access_token = config_helper.get_token()
if not access_token: if not access_token:
log.error("No access token found in config") log.error("No access token found in config")
return return False
# Read app_id from config # Read app_id from config
app_id = config_helper.get_app_id() app_id = config_helper.get_app_id()
if not app_id: if not app_id:
log.error("No app_id found in config") log.error("No app_id found in config")
return return False
log.step(f"Publishing workflow for app: {app_id}") log.step(f"Publishing workflow for app: {app_id}")
@ -51,14 +51,14 @@ def publish_workflow() -> None:
"Sec-Fetch-Mode": "cors", "Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site", "Sec-Fetch-Site": "same-site",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"authorization": f"Bearer {access_token}", **config_helper.console_auth_headers(),
"content-type": "application/json", "content-type": "application/json",
"sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"', "sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
"sec-ch-ua-mobile": "?0", "sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"', "sec-ch-ua-platform": '"macOS"',
} }
cookies = {"locale": "en-US"} cookies = config_helper.console_auth_cookies()
try: try:
# Make the publish request # Make the publish request
@ -83,23 +83,30 @@ def publish_workflow() -> None:
except json.JSONDecodeError: except json.JSONDecodeError:
# Response might be empty or non-JSON # Response might be empty or non-JSON
pass pass
return True
elif response.status_code == 401: elif response.status_code == 401:
log.error("Workflow publish failed: Unauthorized") log.error("Workflow publish failed: Unauthorized")
log.info("Token may have expired. Please run login_admin.py again") log.info("Token may have expired. Please run login_admin.py again")
return False
elif response.status_code == 404: elif response.status_code == 404:
log.error("Workflow publish failed: App not found") log.error("Workflow publish failed: App not found")
log.info("Make sure the app was imported successfully") log.info("Make sure the app was imported successfully")
return False
else: else:
log.error(f"Workflow publish failed with status code: {response.status_code}") log.error(f"Workflow publish failed with status code: {response.status_code}")
log.debug(f"Response: {response.text}") log.debug(f"Response: {response.text}")
return False
except httpx.ConnectError: except httpx.ConnectError:
log.error("Could not connect to Dify API at http://localhost:5001") log.error("Could not connect to Dify API at http://localhost:5001")
log.info("Make sure the API server is running with: ./dev/start-api") log.info("Make sure the API server is running with: ./dev/start-api")
return False
except Exception as e: except Exception as e:
log.error(f"An error occurred: {e}") log.error(f"An error occurred: {e}")
return False
if __name__ == "__main__": if __name__ == "__main__":
publish_workflow() if not publish_workflow():
sys.exit(1)

View File

@ -5,22 +5,27 @@ from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent)) sys.path.append(str(Path(__file__).parent.parent))
import os
import httpx import httpx
from common import Logger, config_helper from common import Logger, config_helper
def setup_admin_account() -> None: def build_admin_config() -> dict[str, str]:
return {
"email": os.getenv("STRESS_TEST_ADMIN_EMAIL", "test@dify.ai"),
"username": os.getenv("STRESS_TEST_ADMIN_USERNAME", "dify"),
"password": os.getenv("STRESS_TEST_ADMIN_PASSWORD", "password123"),
}
def setup_admin_account() -> bool:
"""Setup Dify API with an admin account.""" """Setup Dify API with an admin account."""
log = Logger("SetupAdmin") log = Logger("SetupAdmin")
log.header("Setting up Admin Account") log.header("Setting up Admin Account")
# Admin account credentials admin_config = build_admin_config()
admin_config = {
"email": "test@dify.ai",
"username": "dify",
"password": "password123",
}
# Save credentials to config file # Save credentials to config file
if config_helper.write_config("admin_config", admin_config): if config_helper.write_config("admin_config", admin_config):
@ -52,20 +57,26 @@ def setup_admin_account() -> None:
log.success("Admin account created successfully!") log.success("Admin account created successfully!")
log.key_value("Email", admin_config["email"]) log.key_value("Email", admin_config["email"])
log.key_value("Username", admin_config["username"]) log.key_value("Username", admin_config["username"])
return True
elif response.status_code == 400: elif response.status_code in {400, 403}:
log.warning("Setup may have already been completed or invalid data provided") log.warning("Setup may have already been completed")
log.debug(f"Response: {response.text}") log.debug(f"Response: {response.text}")
return True
else: else:
log.error(f"Setup failed with status code: {response.status_code}") log.error(f"Setup failed with status code: {response.status_code}")
log.debug(f"Response: {response.text}") log.debug(f"Response: {response.text}")
return False
except httpx.ConnectError: except httpx.ConnectError:
log.error("Could not connect to Dify API at http://localhost:5001") log.error("Could not connect to Dify API at http://localhost:5001")
log.info("Make sure the API server is running with: ./dev/start-api") log.info("Make sure the API server is running with: ./dev/start-api")
return False
except Exception as e: except Exception as e:
log.error(f"An error occurred: {e}") log.error(f"An error occurred: {e}")
return False
if __name__ == "__main__": if __name__ == "__main__":
setup_admin_account() if not setup_admin_account():
sys.exit(1)

View File

@ -1,12 +1,56 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import socket import socket
import subprocess import subprocess
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
from common import Logger, ProgressLogger import httpx
from common import Logger, ProgressLogger, config_helper
def build_admin_config() -> dict[str, str]:
return {
"email": os.getenv("STRESS_TEST_ADMIN_EMAIL", "test@dify.ai"),
"username": os.getenv("STRESS_TEST_ADMIN_USERNAME", "dify"),
"password": os.getenv("STRESS_TEST_ADMIN_PASSWORD", "password123"),
}
def get_setup_step(base_url: str = "http://localhost:5001") -> str | None:
try:
response = httpx.get(f"{base_url}/console/api/setup", timeout=5)
if response.status_code == 200:
return response.json().get("step")
except (httpx.HTTPError, ValueError):
return None
return None
def confirm(prompt: str) -> bool:
answer = input(f"\n{prompt} [Y/n]: ").strip().lower()
return answer in ("", "y", "yes")
def confirm_admin_credentials(log: Logger) -> bool:
admin_config = build_admin_config()
setup_step = get_setup_step()
config_helper.write_config("admin_config", admin_config)
if setup_step == "finished":
log.warning("Dify is already initialized; setup will use the existing admin account to log in.")
log.key_value("Admin email", admin_config["email"])
log.info("Set STRESS_TEST_ADMIN_EMAIL and STRESS_TEST_ADMIN_PASSWORD if this is not the right account.")
return confirm("Continue with this admin login?")
log.info("Dify is not initialized; setup will create the first admin account with:")
log.key_value("Admin email", admin_config["email"])
log.key_value("Admin username", admin_config["username"])
log.key_value("Admin password", admin_config["password"])
log.info("Set STRESS_TEST_ADMIN_EMAIL, STRESS_TEST_ADMIN_USERNAME, or STRESS_TEST_ADMIN_PASSWORD to override.")
return confirm("Create/use this admin account?")
def run_script(script_name: str, description: str) -> bool: def run_script(script_name: str, description: str) -> bool:
@ -89,19 +133,20 @@ def main() -> None:
if not dify_running or not mock_running: if not dify_running or not mock_running:
print("\n⚠️ Both services must be running before proceeding.") print("\n⚠️ Both services must be running before proceeding.")
retry = input("\nWould you like to check again? (yes/no): ") if confirm("Would you like to check again?"):
if retry.lower() in ["yes", "y"]:
return main() # Recursively call main to check again return main() # Recursively call main to check again
else: else:
print("❌ Setup cancelled. Please start the required services and try again.") print("❌ Setup cancelled. Please start the required services and try again.")
sys.exit(1) sys.exit(1)
log.success("All required services are running!") log.success("All required services are running!")
input("\nPress Enter to continue with setup...") if not confirm_admin_credentials(log):
print("❌ Setup cancelled. Please set the admin environment variables and try again.")
sys.exit(1)
# Define setup steps # Define setup steps
setup_step = get_setup_step()
setup_steps = [ setup_steps = [
("setup_admin.py", "Creating admin account"),
("login_admin.py", "Logging in and getting access token"), ("login_admin.py", "Logging in and getting access token"),
("install_openai_plugin.py", "Installing OpenAI plugin"), ("install_openai_plugin.py", "Installing OpenAI plugin"),
("configure_openai_plugin.py", "Configuring OpenAI plugin with mock server"), ("configure_openai_plugin.py", "Configuring OpenAI plugin with mock server"),
@ -109,6 +154,8 @@ def main() -> None:
("create_api_key.py", "Creating API key for the app"), ("create_api_key.py", "Creating API key for the app"),
("publish_workflow.py", "Publishing the workflow"), ("publish_workflow.py", "Publishing the workflow"),
] ]
if setup_step != "finished":
setup_steps.insert(0, ("setup_admin.py", "Creating admin account"))
# Create progress logger # Create progress logger
progress = ProgressLogger(len(setup_steps), log) progress = ProgressLogger(len(setup_steps), log)
@ -131,6 +178,18 @@ def main() -> None:
log.error(f"Setup failed at: {failed_step}") log.error(f"Setup failed at: {failed_step}")
log.separator() log.separator()
log.info("Troubleshooting:") log.info("Troubleshooting:")
if failed_step == "login_admin.py":
if get_setup_step() == "finished":
log.list_item(
"Dify is already initialized; set STRESS_TEST_ADMIN_EMAIL and "
"STRESS_TEST_ADMIN_PASSWORD to an existing admin account."
)
else:
admin_config = build_admin_config()
log.list_item(
"Dify is not initialized; setup creates the first admin with "
f"{admin_config['email']} / {admin_config['username']} unless overridden by environment variables."
)
log.list_item("Check if the Dify API server is running (./dev/start-api)") log.list_item("Check if the Dify API server is running (./dev/start-api)")
log.list_item("Check if the mock OpenAI server is running (port 5004)") log.list_item("Check if the mock OpenAI server is running (port 5004)")
log.list_item("Review the error messages above") log.list_item("Review the error messages above")
@ -151,9 +210,7 @@ def main() -> None:
# Optionally run a test # Optionally run a test
log.separator() log.separator()
test_input = input("Would you like to run a test workflow now? (yes/no): ") if confirm("Would you like to run a test workflow now?"):
if test_input.lower() in ["yes", "y"]:
log.step("Running test workflow...") log.step("Running test workflow...")
run_script("run_workflow.py", "Testing workflow with default question") run_script("run_workflow.py", "Testing workflow with default question")

View File

@ -0,0 +1,120 @@
#!/usr/bin/env python3
import base64
import importlib.util
import sys
from pathlib import Path
from common.config_helper import ConfigHelper
def _load_setup_module(name: str):
module_path = Path(__file__).parent / "setup" / f"{name}.py"
spec = importlib.util.spec_from_file_location(f"stress_test_{name}", module_path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _load_stress_test_module(name: str):
module_path = Path(__file__).parent / f"{name}.py"
spec = importlib.util.spec_from_file_location(f"stress_test_{name}", module_path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def test_config_helper_getters_read_state_sections(tmp_path):
helper = ConfigHelper(base_dir=tmp_path)
helper.write_state(
{
"auth": {"access_token": "console-token", "csrf_token": "csrf-token"},
"app": {"app_id": "app-id"},
"api_key": {"token": "app-token"},
}
)
assert helper.get_token() == "console-token"
assert helper.get_csrf_token() == "csrf-token"
assert helper.get_app_id() == "app-id"
assert helper.get_api_key() == "app-token"
assert helper.console_auth_headers() == {
"authorization": "Bearer console-token",
"X-CSRF-Token": "csrf-token",
}
assert helper.console_auth_cookies() == {
"locale": "en-US",
"access_token": "console-token",
"csrf_token": "csrf-token",
}
def test_login_admin_encodes_password_like_web_client():
login_admin = _load_setup_module("login_admin")
encoded = login_admin.encode_sensitive_field("password123")
assert encoded == base64.b64encode(b"password123").decode()
def test_setup_admin_reads_credentials_from_environment(monkeypatch):
setup_admin = _load_setup_module("setup_admin")
monkeypatch.setenv("STRESS_TEST_ADMIN_EMAIL", "real-admin@example.com")
monkeypatch.setenv("STRESS_TEST_ADMIN_USERNAME", "real-admin")
monkeypatch.setenv("STRESS_TEST_ADMIN_PASSWORD", "secret")
assert setup_admin.build_admin_config() == {
"email": "real-admin@example.com",
"username": "real-admin",
"password": "secret",
}
def test_setup_all_reads_credentials_from_environment(monkeypatch):
setup_all = _load_stress_test_module("setup_all")
monkeypatch.setenv("STRESS_TEST_ADMIN_EMAIL", "real-admin@example.com")
monkeypatch.setenv("STRESS_TEST_ADMIN_USERNAME", "real-admin")
monkeypatch.setenv("STRESS_TEST_ADMIN_PASSWORD", "secret")
assert setup_all.build_admin_config() == {
"email": "real-admin@example.com",
"username": "real-admin",
"password": "secret",
}
def test_setup_all_confirm_defaults_to_yes(monkeypatch):
setup_all = _load_stress_test_module("setup_all")
monkeypatch.setattr("builtins.input", lambda prompt: "")
assert setup_all.confirm("Continue?") is True
def test_create_api_key_fails_without_app_id(tmp_path):
create_api_key = _load_setup_module("create_api_key")
create_api_key.config_helper.base_dir = tmp_path
create_api_key.config_helper.write_state({"auth": {"access_token": "token"}})
assert create_api_key.create_api_key() is False
def test_plugin_install_response_without_task_is_non_blocking():
install_openai_plugin = _load_setup_module("install_openai_plugin")
assert install_openai_plugin.is_non_blocking_install_response({"ok": True}) is True
assert install_openai_plugin.is_non_blocking_install_response({}) is True
assert install_openai_plugin.is_non_blocking_install_response({"code": "plugin_error"}) is False
def test_import_response_with_warnings_and_app_id_is_success():
import_workflow_app = _load_setup_module("import_workflow_app")
assert import_workflow_app.is_successful_import_response(
{"status": "completed-with-warnings", "app_id": "app-id"}
)
assert not import_workflow_app.is_successful_import_response({"status": "failed", "app_id": "app-id"})
assert not import_workflow_app.is_successful_import_response({"status": "completed"})