4runr-os 2.9.32 → 2.9.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/gateway/.dockerignore +11 -0
- package/apps/gateway/.eslintrc.json +28 -0
- package/apps/gateway/Dockerfile +122 -0
- package/apps/gateway/Dockerfile.bak +41 -0
- package/apps/gateway/create-test-script.sh +386 -0
- package/apps/gateway/debug-api-responses.sh +94 -0
- package/apps/gateway/debug-failing-tests-v2.sh +81 -0
- package/apps/gateway/debug-failing-tests.sh +89 -0
- package/apps/gateway/debug-responses.sh +83 -0
- package/apps/gateway/debug-test-responses.sh +54 -0
- package/apps/gateway/debug-tests.sh +119 -0
- package/apps/gateway/diagnose-test-failure.sh +53 -0
- package/apps/gateway/find-gateway.sh +37 -0
- package/apps/gateway/jest.config.cjs +50 -0
- package/apps/gateway/load-tests/k6-basic.js +128 -0
- package/apps/gateway/load-tests/k6-rate-limit.js +57 -0
- package/apps/gateway/minimal-test.sh +60 -0
- package/apps/gateway/package.json +66 -0
- package/apps/gateway/public/sentinel-dashboard.html +428 -0
- package/apps/gateway/quick-debug.sh +70 -0
- package/apps/gateway/scripts/seed-api-key.ts +63 -0
- package/apps/gateway/scripts/setup-test-env.sh +67 -0
- package/apps/gateway/simple-test.sh +72 -0
- package/apps/gateway/src/__tests__/auth.test.ts +272 -0
- package/apps/gateway/src/__tests__/devkit-api.test.ts +268 -0
- package/apps/gateway/src/__tests__/integration/authentication.test.ts +155 -0
- package/apps/gateway/src/__tests__/integration/e2e-comprehensive-fixed.test.ts +368 -0
- package/apps/gateway/src/__tests__/integration/e2e-workflow.test.ts +239 -0
- package/apps/gateway/src/__tests__/integration/helpers/test-server.ts +142 -0
- package/apps/gateway/src/__tests__/integration/idempotency.test.ts +213 -0
- package/apps/gateway/src/__tests__/integration/postgres-persistence.test.ts +173 -0
- package/apps/gateway/src/__tests__/integration/rate-limiting.test.ts +148 -0
- package/apps/gateway/src/__tests__/integration/sentinel.test.ts +152 -0
- package/apps/gateway/src/__tests__/no-persistence-mode.test.ts +180 -0
- package/apps/gateway/src/__tests__/rateLimit.test.ts +107 -0
- package/apps/gateway/src/__tests__/validation.test.ts +290 -0
- package/apps/gateway/src/adapters/redis-sentinel-publisher.ts +47 -0
- package/apps/gateway/src/adapters/sentinel-event-stream.ts +106 -0
- package/apps/gateway/src/agents/definitions-simple.ts +531 -0
- package/apps/gateway/src/agents/definitions.ts +297 -0
- package/apps/gateway/src/agents/local-model-provider.ts +219 -0
- package/apps/gateway/src/agents/tools.ts +163 -0
- package/apps/gateway/src/ai-providers/anthropic-provider.ts +194 -0
- package/apps/gateway/src/ai-providers/index.ts +10 -0
- package/apps/gateway/src/ai-providers/openai-provider.ts +193 -0
- package/apps/gateway/src/ai-providers/provider-manager.ts +160 -0
- package/apps/gateway/src/ai-providers/redis-credentials-store.ts +220 -0
- package/apps/gateway/src/ai-providers/types.ts +75 -0
- package/apps/gateway/src/config/persistence.ts +38 -0
- package/apps/gateway/src/crypto/envelope.ts +184 -0
- package/apps/gateway/src/db/prisma.ts +58 -0
- package/apps/gateway/src/db/redis.ts +95 -0
- package/apps/gateway/src/devkit/agents-api.ts +486 -0
- package/apps/gateway/src/devkit/metrics-parser.ts +152 -0
- package/apps/gateway/src/devkit/middleware.ts +53 -0
- package/apps/gateway/src/devkit/routes.ts +344 -0
- package/apps/gateway/src/devkit/tools-api.ts +251 -0
- package/apps/gateway/src/health/index.ts +257 -0
- package/apps/gateway/src/index.ts +1288 -0
- package/apps/gateway/src/metrics/index.ts +218 -0
- package/apps/gateway/src/middleware/auth.ts +118 -0
- package/apps/gateway/src/middleware/authApiKey.ts +156 -0
- package/apps/gateway/src/middleware/authJwt.ts +129 -0
- package/apps/gateway/src/middleware/correlationId.ts +36 -0
- package/apps/gateway/src/middleware/ddos-protection.ts +286 -0
- package/apps/gateway/src/middleware/errorHandler.ts +168 -0
- package/apps/gateway/src/middleware/mfa.ts +137 -0
- package/apps/gateway/src/middleware/rateLimit.ts +104 -0
- package/apps/gateway/src/middleware/rbac.ts +301 -0
- package/apps/gateway/src/middleware/security.ts +116 -0
- package/apps/gateway/src/middleware/validate.ts +194 -0
- package/apps/gateway/src/middleware/validate.ts.backup +153 -0
- package/apps/gateway/src/queue/config.ts +61 -0
- package/apps/gateway/src/queue/index.ts +229 -0
- package/apps/gateway/src/queue/processor.ts +461 -0
- package/apps/gateway/src/routes/ai-providers-simple.ts +166 -0
- package/apps/gateway/src/routes/ai-providers.ts +235 -0
- package/apps/gateway/src/routes/chats.ts +177 -0
- package/apps/gateway/src/routes/gdpr.ts +299 -0
- package/apps/gateway/src/routes/mfa.ts +254 -0
- package/apps/gateway/src/routes/sentinel-policies.ts +204 -0
- package/apps/gateway/src/routes/sentinel-predictive.ts +119 -0
- package/apps/gateway/src/routes/shield.ts +114 -0
- package/apps/gateway/src/routes/tool-credentials.ts +282 -0
- package/apps/gateway/src/routes/tool-proxy.ts +303 -0
- package/apps/gateway/src/runs/index.ts +34 -0
- package/apps/gateway/src/runs/memoryRunStore.ts +105 -0
- package/apps/gateway/src/runs/postgresRunStore.ts +186 -0
- package/apps/gateway/src/runs/runStore.ts +17 -0
- package/apps/gateway/src/runs/types.ts +58 -0
- package/apps/gateway/src/schemas/runs.ts +115 -0
- package/apps/gateway/src/types/fastify-rate-limit.d.ts +12 -0
- package/apps/gateway/src/utils/circuit-breaker.ts +134 -0
- package/apps/gateway/src/utils/log-encryption.ts +212 -0
- package/apps/gateway/test-all-individual.sh +69 -0
- package/apps/gateway/test-all-phases-final.sh +568 -0
- package/apps/gateway/test-all-phases-fixed-v2.sh +523 -0
- package/apps/gateway/test-all-phases-fixed.sh +385 -0
- package/apps/gateway/test-all-phases.sh +663 -0
- package/apps/gateway/test-concurrency.sh +60 -0
- package/apps/gateway/test-debug.sh +44 -0
- package/apps/gateway/test-e2e.sh +96 -0
- package/apps/gateway/test-extraction.sh +48 -0
- package/apps/gateway/test-full-operations-final.sh +328 -0
- package/apps/gateway/test-full-operations.sh +326 -0
- package/apps/gateway/test-idempotency-only.sh +108 -0
- package/apps/gateway/test-idempotency.sh +97 -0
- package/apps/gateway/test-individual.sh +126 -0
- package/apps/gateway/test-queue.sh +94 -0
- package/apps/gateway/tsconfig.json +32 -0
- package/apps/gateway/update-and-test.sh +36 -0
- package/dist/tui-handlers.js +174 -21
- package/dist/tui-handlers.js.map +1 -1
- package/mk3-tui/src/ui/layout.rs +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": true,
|
|
3
|
+
"parser": "@typescript-eslint/parser",
|
|
4
|
+
"parserOptions": {
|
|
5
|
+
"ecmaVersion": 2022,
|
|
6
|
+
"sourceType": "module"
|
|
7
|
+
},
|
|
8
|
+
"plugins": ["@typescript-eslint"],
|
|
9
|
+
"extends": [
|
|
10
|
+
"eslint:recommended",
|
|
11
|
+
"plugin:@typescript-eslint/recommended"
|
|
12
|
+
],
|
|
13
|
+
"rules": {
|
|
14
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
15
|
+
"@typescript-eslint/no-unused-vars": "warn",
|
|
16
|
+
"@typescript-eslint/no-non-null-assertion": "off"
|
|
17
|
+
},
|
|
18
|
+
"env": {
|
|
19
|
+
"node": true,
|
|
20
|
+
"es2022": true
|
|
21
|
+
},
|
|
22
|
+
"ignorePatterns": [
|
|
23
|
+
"**/__tests__/**",
|
|
24
|
+
"**/*.test.ts",
|
|
25
|
+
"dist/**"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
FROM node:20-slim AS base
|
|
2
|
+
WORKDIR /workspace
|
|
3
|
+
RUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/*
|
|
4
|
+
|
|
5
|
+
FROM base AS deps
|
|
6
|
+
RUN apt-get update && apt-get install -y python3 make g++ openssl libssl-dev && rm -rf /var/lib/apt/lists/*
|
|
7
|
+
COPY pnpm-workspace.yaml package.json tsconfig.json ./
|
|
8
|
+
# Copy .npmrc if it exists (optional - create empty if missing)
|
|
9
|
+
COPY apps/gateway ./apps/gateway
|
|
10
|
+
# Copy packages directory but we'll remove client-side ones immediately
|
|
11
|
+
COPY packages ./packages
|
|
12
|
+
# Remove client-side packages BEFORE pnpm reads the workspace (os-cli depends on unpublished @4runr/devkit)
|
|
13
|
+
RUN rm -rf packages/os-cli packages/cli packages/cli-tool packages/cli-tool-standalone packages/devkit || true
|
|
14
|
+
COPY prisma ./prisma
|
|
15
|
+
COPY src ./src
|
|
16
|
+
# Create .npmrc if it doesn't exist (pnpm will work without it)
|
|
17
|
+
RUN touch .npmrc || true
|
|
18
|
+
RUN corepack enable && corepack prepare pnpm@9.12.1 --activate
|
|
19
|
+
# Set node-linker to hoisted for flat node_modules structure
|
|
20
|
+
RUN pnpm config set node-linker hoisted
|
|
21
|
+
# Skip Prisma generation during install (we'll generate it in build if needed)
|
|
22
|
+
ENV PRISMA_SKIP_POSTINSTALL_GENERATE=1
|
|
23
|
+
RUN pnpm install --filter @4runr/shared --filter @4runr/sentinel --filter @4runr/gateway --workspace-root
|
|
24
|
+
|
|
25
|
+
FROM deps AS build
|
|
26
|
+
# Clean shared and sentinel package dists to ensure fresh build with updated types
|
|
27
|
+
RUN rm -rf packages/shared/dist packages/shared/.tsbuildinfo packages/sentinel/dist packages/sentinel/.tsbuildinfo
|
|
28
|
+
RUN pnpm --filter @4runr/shared run build
|
|
29
|
+
RUN pnpm --filter @4runr/sentinel run build
|
|
30
|
+
# Generate Prisma client before building gateway
|
|
31
|
+
# Use dummy DATABASE_URL for generation (just needs schema, not actual connection)
|
|
32
|
+
# Prisma schema is at /workspace/prisma/schema.prisma
|
|
33
|
+
RUN cd /workspace/apps/gateway && \
|
|
34
|
+
export DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" && \
|
|
35
|
+
pnpm db:generate && \
|
|
36
|
+
(test -d node_modules/.prisma/client || test -d ../../node_modules/.prisma/client) && \
|
|
37
|
+
echo "✓ Prisma client generated" || echo "✗ Prisma client generation failed"
|
|
38
|
+
# Compile root src/crypto folder (used by gateway routes)
|
|
39
|
+
# Create a temporary tsconfig for compiling just the crypto folder
|
|
40
|
+
RUN mkdir -p dist/src/crypto && \
|
|
41
|
+
echo '{"compilerOptions":{"target":"ES2022","module":"ES2022","moduleResolution":"bundler","esModuleInterop":true,"skipLibCheck":true,"outDir":"dist/src/crypto","rootDir":"src/crypto"},"include":["src/crypto/envelope.ts"]}' > /tmp/crypto-tsconfig.json && \
|
|
42
|
+
npx tsc --project /tmp/crypto-tsconfig.json 2>&1 | head -10 && \
|
|
43
|
+
test -f dist/src/crypto/envelope.js && echo "✓ Compiled envelope.js" || echo "⚠ envelope.js compilation may have failed"
|
|
44
|
+
|
|
45
|
+
RUN rm -rf apps/gateway/dist && \
|
|
46
|
+
pnpm --filter @4runr/gateway run build && \
|
|
47
|
+
(test -f apps/gateway/dist/index.js || test -f apps/gateway/dist/apps/gateway/src/index.js) && \
|
|
48
|
+
echo "✓ Gateway build completed successfully" || \
|
|
49
|
+
(echo "✗ Build failed: index.js not found" && exit 1)
|
|
50
|
+
|
|
51
|
+
FROM base AS runner
|
|
52
|
+
WORKDIR /workspace
|
|
53
|
+
RUN apt-get update && apt-get install -y openssl ca-certificates curl && rm -rf /var/lib/apt/lists/*
|
|
54
|
+
# Copy package files, config, and built artifacts
|
|
55
|
+
COPY --from=build /workspace/package.json ./package.json
|
|
56
|
+
COPY --from=build /workspace/pnpm-workspace.yaml ./pnpm-workspace.yaml
|
|
57
|
+
# .npmrc is optional - create empty if it doesn't exist
|
|
58
|
+
RUN touch .npmrc || true
|
|
59
|
+
COPY --from=build /workspace/packages ./packages
|
|
60
|
+
COPY --from=build /workspace/apps/gateway/package.json ./apps/gateway/package.json
|
|
61
|
+
COPY --from=build /workspace/apps/gateway/dist ./apps/gateway/dist
|
|
62
|
+
# Copy public directory (static files like dashboard)
|
|
63
|
+
COPY --from=deps /workspace/apps/gateway/public ./apps/gateway/public
|
|
64
|
+
# Copy node_modules from build stage (hoisted node-linker creates flat structure)
|
|
65
|
+
# Use --chown to set ownership during copy to avoid breaking symlinks
|
|
66
|
+
COPY --from=build --chown=node:node /workspace/node_modules ./node_modules
|
|
67
|
+
# Copy Prisma schema (needed for runtime client generation if needed)
|
|
68
|
+
COPY --from=build /workspace/prisma ./prisma
|
|
69
|
+
# Copy src directory (contains crypto and memory-db for AI providers)
|
|
70
|
+
COPY --from=build /workspace/src ./src
|
|
71
|
+
# Copy compiled crypto (if it was compiled)
|
|
72
|
+
RUN mkdir -p dist/src && (cp -r /workspace/dist/src/* dist/src/ 2>/dev/null || echo "Note: No compiled src found")
|
|
73
|
+
# Verify Prisma client was copied (could be in root node_modules with hoisted linker)
|
|
74
|
+
RUN (test -d node_modules/.prisma/client || test -d apps/gateway/node_modules/.prisma/client) && \
|
|
75
|
+
echo "✓ Prisma client found" || \
|
|
76
|
+
echo "⚠ Prisma client not found - checking..." && \
|
|
77
|
+
find node_modules -name ".prisma" -type d 2>/dev/null | head -5 || \
|
|
78
|
+
echo "⚠ Will generate Prisma client at runtime if needed"
|
|
79
|
+
# Verify shared package is accessible
|
|
80
|
+
RUN test -d packages/shared/dist && echo "✓ shared/dist exists" || echo "✗ shared/dist missing"
|
|
81
|
+
RUN test -f packages/shared/dist/index.js && echo "✓ shared/dist/index.js exists" || echo "✗ shared/dist/index.js missing"
|
|
82
|
+
# Check if @4runr/shared exists and if it's a symlink
|
|
83
|
+
# If not, create it (symlinks don't need special ownership)
|
|
84
|
+
RUN if [ ! -L node_modules/@4runr/shared ] && [ ! -d node_modules/@4runr/shared ]; then \
|
|
85
|
+
echo "✗ @4runr/shared NOT found in node_modules, creating symlink..."; \
|
|
86
|
+
mkdir -p node_modules/@4runr && \
|
|
87
|
+
ln -s ../../packages/shared node_modules/@4runr/shared && \
|
|
88
|
+
echo "✓ Created symlink: node_modules/@4runr/shared -> ../../packages/shared"; \
|
|
89
|
+
fi
|
|
90
|
+
# Verify the shared package's package.json exists (for ESM resolution)
|
|
91
|
+
RUN test -f node_modules/@4runr/shared/package.json && echo "✓ @4runr/shared/package.json exists" || echo "✗ @4runr/shared/package.json missing"
|
|
92
|
+
# Verify the package.json has the correct exports field for ESM resolution
|
|
93
|
+
RUN node -e "try { const pkg = require('./node_modules/@4runr/shared/package.json'); if (pkg.exports && pkg.exports['.']) { console.log('✓ package.json exports field configured correctly'); } else { console.error('✗ package.json missing exports field'); process.exit(1); } } catch(e) { console.error('✗ Error reading shared package.json:', e.message); process.exit(1); }"
|
|
94
|
+
# Verify Sentinel package is accessible
|
|
95
|
+
RUN test -d packages/sentinel/dist && echo "✓ sentinel/dist exists" || echo "✗ sentinel/dist missing"
|
|
96
|
+
RUN test -f packages/sentinel/dist/index.js && echo "✓ sentinel/dist/index.js exists" || echo "✗ sentinel/dist/index.js missing"
|
|
97
|
+
# Check if @4runr/sentinel exists and if it's a symlink
|
|
98
|
+
# If not, create it (symlinks don't need special ownership)
|
|
99
|
+
RUN if [ ! -L node_modules/@4runr/sentinel ] && [ ! -d node_modules/@4runr/sentinel ]; then \
|
|
100
|
+
echo "✗ @4runr/sentinel NOT found in node_modules, creating symlink..."; \
|
|
101
|
+
mkdir -p node_modules/@4runr && \
|
|
102
|
+
ln -s ../../packages/sentinel node_modules/@4runr/sentinel && \
|
|
103
|
+
echo "✓ Created symlink: node_modules/@4runr/sentinel -> ../../packages/sentinel"; \
|
|
104
|
+
fi
|
|
105
|
+
# Verify the Sentinel package's package.json exists (for ESM resolution)
|
|
106
|
+
RUN test -f node_modules/@4runr/sentinel/package.json && echo "✓ @4runr/sentinel/package.json exists" || echo "✗ @4runr/sentinel/package.json missing"
|
|
107
|
+
# Verify the package.json has the correct exports field for ESM resolution
|
|
108
|
+
RUN node -e "try { const pkg = require('./node_modules/@4runr/sentinel/package.json'); if (pkg.exports && pkg.exports['.']) { console.log('✓ Sentinel package.json exports field configured correctly'); } else { console.error('✗ Sentinel package.json missing exports field'); process.exit(1); } } catch(e) { console.error('✗ Error reading Sentinel package.json:', e.message); process.exit(1); }"
|
|
109
|
+
# Verify fastify exists (should be directly in node_modules with hoisted node-linker)
|
|
110
|
+
RUN test -d node_modules/fastify && echo "✓ fastify found in node_modules" || \
|
|
111
|
+
(echo "✗ fastify NOT found!" && ls -la node_modules/ | head -40)
|
|
112
|
+
RUN test -f node_modules/fastify/package.json && echo "✓ fastify package.json exists" || echo "✗ fastify package.json missing"
|
|
113
|
+
# Test Node.js ESM resolution from the gateway dist directory
|
|
114
|
+
RUN cd apps/gateway/dist && \
|
|
115
|
+
node --input-type=module -e "import('fastify').then(() => console.log('✓ Node.js can resolve fastify from dist')).catch(e => console.error('✗ Cannot resolve:', e.message))" || \
|
|
116
|
+
echo "Testing from workspace root:" && \
|
|
117
|
+
cd /workspace && \
|
|
118
|
+
node --input-type=module -e "import('fastify').then(() => console.log('✓ Node.js can resolve fastify from root')).catch(e => console.error('✗ Cannot resolve from root:', e.message))"
|
|
119
|
+
USER node
|
|
120
|
+
WORKDIR /workspace
|
|
121
|
+
# Run from workspace root - Node.js ESM should find node_modules at /workspace/node_modules
|
|
122
|
+
CMD ["node", "apps/gateway/dist/apps/gateway/src/index.js"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
FROM node:18-alpine AS base
|
|
2
|
+
|
|
3
|
+
# Install dependencies only when needed
|
|
4
|
+
FROM base AS deps
|
|
5
|
+
RUN apk add --no-cache libc6-compat
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
# Install dependencies based on the preferred package manager
|
|
9
|
+
COPY package.json ./
|
|
10
|
+
RUN npm install
|
|
11
|
+
|
|
12
|
+
# Rebuild the source code only when needed
|
|
13
|
+
FROM base AS builder
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
16
|
+
COPY . .
|
|
17
|
+
|
|
18
|
+
# Build the application
|
|
19
|
+
RUN npm run build
|
|
20
|
+
|
|
21
|
+
# Production image, copy all the files and run the app
|
|
22
|
+
FROM base AS runner
|
|
23
|
+
WORKDIR /app
|
|
24
|
+
|
|
25
|
+
ENV NODE_ENV=production
|
|
26
|
+
|
|
27
|
+
RUN addgroup --system --gid 1001 nodejs
|
|
28
|
+
RUN adduser --system --uid 1001 4runr
|
|
29
|
+
|
|
30
|
+
COPY --from=builder /app/dist ./dist
|
|
31
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
32
|
+
COPY --from=builder /app/package.json ./package.json
|
|
33
|
+
|
|
34
|
+
USER 4runr
|
|
35
|
+
|
|
36
|
+
EXPOSE 3000
|
|
37
|
+
|
|
38
|
+
ENV PORT=3000
|
|
39
|
+
ENV HOST=0.0.0.0
|
|
40
|
+
|
|
41
|
+
CMD ["node", "dist/index.js"]
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Script to create the clean test-all-phases.sh file on the server
|
|
3
|
+
|
|
4
|
+
cat > /opt/4runr/gateway/test-all-phases.sh << 'EOFTEST'
|
|
5
|
+
#!/bin/bash
|
|
6
|
+
# Comprehensive test suite for all phases
|
|
7
|
+
# Tests Phase 1-6 features end-to-end
|
|
8
|
+
|
|
9
|
+
set +e # Don't exit on errors, we want to see all test results
|
|
10
|
+
|
|
11
|
+
GATEWAY_URL="${TEST_SERVER_URL:-http://localhost:3000}"
|
|
12
|
+
API_KEY="${TEST_API_KEY:-test-key-ce67dabb0d4e27e3d72877c921b89cae}"
|
|
13
|
+
|
|
14
|
+
# Colors for output
|
|
15
|
+
RED='\033[0;31m'
|
|
16
|
+
GREEN='\033[0;32m'
|
|
17
|
+
YELLOW='\033[1;33m'
|
|
18
|
+
NC='\033[0m' # No Color
|
|
19
|
+
|
|
20
|
+
# Test counters
|
|
21
|
+
TOTAL_TESTS=0
|
|
22
|
+
PASSED_TESTS=0
|
|
23
|
+
FAILED_TESTS=0
|
|
24
|
+
|
|
25
|
+
# Test result tracking
|
|
26
|
+
FAILED_TEST_NAMES=()
|
|
27
|
+
|
|
28
|
+
# Generate a valid UUID v4
|
|
29
|
+
generate_uuid() {
|
|
30
|
+
if command -v uuidgen > /dev/null 2>&1; then
|
|
31
|
+
uuidgen
|
|
32
|
+
else
|
|
33
|
+
cat /proc/sys/kernel/random/uuid 2>/dev/null || \
|
|
34
|
+
echo "$(od -x /dev/urandom | head -1 | awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}' | tr '[:upper:]' '[:lower:]')"
|
|
35
|
+
fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Helper function to run a test
|
|
39
|
+
run_test() {
|
|
40
|
+
local test_name="$1"
|
|
41
|
+
local test_command="$2"
|
|
42
|
+
|
|
43
|
+
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
|
44
|
+
echo -n "Testing: $test_name... "
|
|
45
|
+
|
|
46
|
+
if eval "$test_command" > /tmp/test_output_$$.log 2>&1; then
|
|
47
|
+
echo -e "${GREEN}✓ PASSED${NC}"
|
|
48
|
+
PASSED_TESTS=$((PASSED_TESTS + 1))
|
|
49
|
+
return 0
|
|
50
|
+
else
|
|
51
|
+
echo -e "${RED}✗ FAILED${NC}"
|
|
52
|
+
FAILED_TESTS=$((FAILED_TESTS + 1))
|
|
53
|
+
FAILED_TEST_NAMES+=("$test_name")
|
|
54
|
+
echo " Error output:"
|
|
55
|
+
cat /tmp/test_output_$$.log | sed 's/^/ /'
|
|
56
|
+
return 1
|
|
57
|
+
fi
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
echo "========================================="
|
|
61
|
+
echo "4Runr Gateway - Comprehensive Phase Test"
|
|
62
|
+
echo "========================================="
|
|
63
|
+
echo ""
|
|
64
|
+
echo "Gateway URL: $GATEWAY_URL"
|
|
65
|
+
echo "API Key: ${API_KEY:0:20}..."
|
|
66
|
+
echo ""
|
|
67
|
+
|
|
68
|
+
# Wait for server to be ready
|
|
69
|
+
echo "Waiting for server to be ready..."
|
|
70
|
+
for i in {1..30}; do
|
|
71
|
+
if curl -sf "$GATEWAY_URL/health" > /dev/null 2>&1; then
|
|
72
|
+
echo "✓ Server is ready"
|
|
73
|
+
break
|
|
74
|
+
fi
|
|
75
|
+
if [ $i -eq 30 ]; then
|
|
76
|
+
echo "❌ Server is not ready after 30 seconds"
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
sleep 1
|
|
80
|
+
done
|
|
81
|
+
|
|
82
|
+
echo ""
|
|
83
|
+
echo "========================================="
|
|
84
|
+
echo "Phase 1-3: Core Features"
|
|
85
|
+
echo "========================================="
|
|
86
|
+
echo ""
|
|
87
|
+
|
|
88
|
+
# Phase 1: Idempotency
|
|
89
|
+
echo "--- Phase 1: Idempotency ---"
|
|
90
|
+
IDEMPOTENCY_KEY=$(generate_uuid)
|
|
91
|
+
IDEMPOTENT_RESPONSE=$(curl -sf --max-time 5 -X POST "$GATEWAY_URL/api/runs" \
|
|
92
|
+
-H "x-api-key: $API_KEY" \
|
|
93
|
+
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
|
|
94
|
+
-H "Content-Type: application/json" \
|
|
95
|
+
-d '{"name":"Idempotency Test","input":{"test":"idempotency"}}' 2>/dev/null)
|
|
96
|
+
|
|
97
|
+
run_test "Idempotency: Create run with idempotency key" \
|
|
98
|
+
"echo \"$IDEMPOTENT_RESPONSE\" | grep -q '\"run\"\|\"success\"'"
|
|
99
|
+
|
|
100
|
+
FIRST_RUN_ID=$(echo "$IDEMPOTENT_RESPONSE" | grep -o '"run":{[^}]*"id":"[^"]*' | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
|
|
101
|
+
if [ -z "$FIRST_RUN_ID" ]; then
|
|
102
|
+
FIRST_RUN_ID=$(echo "$IDEMPOTENT_RESPONSE" | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
if [ ! -z "$FIRST_RUN_ID" ]; then
|
|
106
|
+
SECOND_RESPONSE=$(curl -sf --max-time 5 -X POST "$GATEWAY_URL/api/runs" \
|
|
107
|
+
-H "x-api-key: $API_KEY" \
|
|
108
|
+
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
|
|
109
|
+
-H "Content-Type: application/json" \
|
|
110
|
+
-d '{"name":"Idempotency Test","input":{"test":"idempotency"}}' 2>/dev/null)
|
|
111
|
+
|
|
112
|
+
SECOND_RUN_ID=$(echo "$SECOND_RESPONSE" | grep -o '"run":{[^}]*"id":"[^"]*' | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
|
|
113
|
+
if [ -z "$SECOND_RUN_ID" ]; then
|
|
114
|
+
SECOND_RUN_ID=$(echo "$SECOND_RESPONSE" | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
run_test "Idempotency: Duplicate request returns same run" \
|
|
118
|
+
"[ ! -z \"$SECOND_RUN_ID\" ] && [ \"$FIRST_RUN_ID\" = \"$SECOND_RUN_ID\" ]"
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Phase 2: Structured Logging & Metrics
|
|
122
|
+
echo ""
|
|
123
|
+
echo "--- Phase 2: Logging & Metrics ---"
|
|
124
|
+
run_test "Metrics: Prometheus metrics endpoint accessible" \
|
|
125
|
+
"curl -sf '$GATEWAY_URL/metrics' | grep -q 'http_request_total'"
|
|
126
|
+
|
|
127
|
+
run_test "Metrics: Metrics include run metrics" \
|
|
128
|
+
"curl -sf '$GATEWAY_URL/metrics' | grep -q 'runs_created_total'"
|
|
129
|
+
|
|
130
|
+
# Phase 3: Queue System
|
|
131
|
+
echo ""
|
|
132
|
+
echo "--- Phase 3: Queue System ---"
|
|
133
|
+
QUEUE_RESPONSE=$(curl -sf --max-time 5 -X POST "$GATEWAY_URL/api/runs" \
|
|
134
|
+
-H "x-api-key: $API_KEY" \
|
|
135
|
+
-H "Content-Type: application/json" \
|
|
136
|
+
-d '{"name":"Queue Test","input":{"test":"queue"}}' 2>/dev/null)
|
|
137
|
+
|
|
138
|
+
QUEUE_RUN_ID=$(echo "$QUEUE_RESPONSE" | grep -o '"run":{[^}]*"id":"[^"]*' | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
|
|
139
|
+
if [ -z "$QUEUE_RUN_ID" ]; then
|
|
140
|
+
QUEUE_RUN_ID=$(echo "$QUEUE_RESPONSE" | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
run_test "Queue: Create run successfully" \
|
|
144
|
+
"[ ! -z \"$QUEUE_RUN_ID\" ]"
|
|
145
|
+
|
|
146
|
+
if [ ! -z "$QUEUE_RUN_ID" ]; then
|
|
147
|
+
START_RESPONSE=$(curl -sf --max-time 5 -X POST "$GATEWAY_URL/api/runs/$QUEUE_RUN_ID/start" \
|
|
148
|
+
-H "x-api-key: $API_KEY" \
|
|
149
|
+
-H "Content-Type: application/json" \
|
|
150
|
+
-d '{"priority":"high"}' 2>/dev/null)
|
|
151
|
+
|
|
152
|
+
run_test "Queue: Start run (should queue)" \
|
|
153
|
+
"echo \"$START_RESPONSE\" | grep -q 'queued\|success'"
|
|
154
|
+
|
|
155
|
+
sleep 2
|
|
156
|
+
|
|
157
|
+
STATUS_RESPONSE=$(curl -sf --max-time 5 "$GATEWAY_URL/api/runs/$QUEUE_RUN_ID" \
|
|
158
|
+
-H "x-api-key: $API_KEY" 2>/dev/null)
|
|
159
|
+
|
|
160
|
+
run_test "Queue: Run status updated" \
|
|
161
|
+
"echo \"$STATUS_RESPONSE\" | grep -q '\"status\"'"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
echo ""
|
|
165
|
+
echo "========================================="
|
|
166
|
+
echo "Phase 4: Production Hardening"
|
|
167
|
+
echo "========================================="
|
|
168
|
+
echo ""
|
|
169
|
+
|
|
170
|
+
# Phase 4.1: Security Headers
|
|
171
|
+
echo "--- Phase 4.1: Security Headers ---"
|
|
172
|
+
run_test "Security: Content-Security-Policy header present" \
|
|
173
|
+
"curl -sf -I '$GATEWAY_URL/health' | grep -qi 'content-security-policy'"
|
|
174
|
+
|
|
175
|
+
run_test "Security: X-Frame-Options header present" \
|
|
176
|
+
"curl -sf -I '$GATEWAY_URL/health' | grep -qi 'x-frame-options'"
|
|
177
|
+
|
|
178
|
+
run_test "Security: X-Content-Type-Options header present" \
|
|
179
|
+
"curl -sf -I '$GATEWAY_URL/health' | grep -qi 'x-content-type-options'"
|
|
180
|
+
|
|
181
|
+
run_test "Security: Server header removed" \
|
|
182
|
+
"! curl -sf -I '$GATEWAY_URL/health' | grep -qi '^server:'"
|
|
183
|
+
|
|
184
|
+
# Phase 4.2: Error Handling
|
|
185
|
+
echo ""
|
|
186
|
+
echo "--- Phase 4.2: Error Handling ---"
|
|
187
|
+
run_test "Error Handling: 404 error includes correlation ID" \
|
|
188
|
+
"curl -s '$GATEWAY_URL/api/runs/00000000-0000-0000-0000-000000000000' \
|
|
189
|
+
-H 'x-api-key: $API_KEY' | grep -q 'error'"
|
|
190
|
+
|
|
191
|
+
run_test "Error Handling: Validation error includes details" \
|
|
192
|
+
"curl -s -X POST '$GATEWAY_URL/api/runs' \
|
|
193
|
+
-H 'x-api-key: $API_KEY' \
|
|
194
|
+
-H 'Content-Type: application/json' \
|
|
195
|
+
-d '{\"input\":{}}' | grep -q 'details'"
|
|
196
|
+
|
|
197
|
+
run_test "Error Handling: Error response includes timestamp" \
|
|
198
|
+
"curl -s '$GATEWAY_URL/api/runs/invalid-id' \
|
|
199
|
+
-H 'x-api-key: $API_KEY' | grep -q 'timestamp'"
|
|
200
|
+
|
|
201
|
+
# Phase 4.3: Health Checks
|
|
202
|
+
echo ""
|
|
203
|
+
echo "--- Phase 4.3: Health Checks ---"
|
|
204
|
+
run_test "Health: Liveness endpoint returns 200" \
|
|
205
|
+
"curl -sf '$GATEWAY_URL/health' | grep -q '\"status\"'"
|
|
206
|
+
|
|
207
|
+
run_test "Health: Readiness endpoint returns 200" \
|
|
208
|
+
"curl -sf '$GATEWAY_URL/ready' | grep -q '\"ready\"'"
|
|
209
|
+
|
|
210
|
+
run_test "Health: Readiness includes dependency checks" \
|
|
211
|
+
"curl -sf '$GATEWAY_URL/ready' | grep -q '\"checks\"'"
|
|
212
|
+
|
|
213
|
+
run_test "Health: Startup endpoint accessible" \
|
|
214
|
+
"curl -sf '$GATEWAY_URL/startup' | grep -q '\"started\"'"
|
|
215
|
+
|
|
216
|
+
echo ""
|
|
217
|
+
echo "========================================="
|
|
218
|
+
echo "Phase 5: Testing & Validation"
|
|
219
|
+
echo "========================================="
|
|
220
|
+
echo ""
|
|
221
|
+
|
|
222
|
+
# Phase 5: Integration Tests
|
|
223
|
+
echo "--- Phase 5: Integration Tests ---"
|
|
224
|
+
run_test "Integration: Can run integration tests" \
|
|
225
|
+
"cd /opt/4runr/gateway && pnpm test:integration -- simple.test.ts 2>&1 | grep -q 'PASS'"
|
|
226
|
+
|
|
227
|
+
echo ""
|
|
228
|
+
echo "========================================="
|
|
229
|
+
echo "End-to-End Workflow Tests"
|
|
230
|
+
echo "========================================="
|
|
231
|
+
echo ""
|
|
232
|
+
|
|
233
|
+
# Complete Workflow
|
|
234
|
+
echo "--- Complete Run Workflow ---"
|
|
235
|
+
WORKFLOW_RESPONSE=$(curl -sf -X POST "$GATEWAY_URL/api/runs" \
|
|
236
|
+
-H "x-api-key: $API_KEY" \
|
|
237
|
+
-H "Content-Type: application/json" \
|
|
238
|
+
-d '{"name":"E2E Workflow Test","input":{"test":"workflow"}}' 2>/dev/null)
|
|
239
|
+
|
|
240
|
+
# Extract run ID - try multiple patterns
|
|
241
|
+
WORKFLOW_RUN_ID=$(echo "$WORKFLOW_RESPONSE" | grep -o '"run"[^}]*"id":"[^"]*' | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
|
|
242
|
+
if [ -z "$WORKFLOW_RUN_ID" ]; then
|
|
243
|
+
# Try simpler pattern
|
|
244
|
+
WORKFLOW_RUN_ID=$(echo "$WORKFLOW_RESPONSE" | grep -o '"id":"[a-f0-9-]\{36\}' | cut -d'"' -f4 | head -1)
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
run_test "E2E: Create run" \
|
|
248
|
+
"[ ! -z \"$WORKFLOW_RUN_ID\" ]"
|
|
249
|
+
|
|
250
|
+
if [ ! -z "$WORKFLOW_RUN_ID" ]; then
|
|
251
|
+
run_test "E2E: Start run" \
|
|
252
|
+
"curl -sf -X POST '$GATEWAY_URL/api/runs/$WORKFLOW_RUN_ID/start' \
|
|
253
|
+
-H 'x-api-key: $API_KEY' \
|
|
254
|
+
-H 'Content-Type: application/json' \
|
|
255
|
+
-d '{\"priority\":\"normal\"}' | grep -q '\"queued\"'"
|
|
256
|
+
|
|
257
|
+
run_test "E2E: Get run status" \
|
|
258
|
+
"curl -sf '$GATEWAY_URL/api/runs/$WORKFLOW_RUN_ID' \
|
|
259
|
+
-H 'x-api-key: $API_KEY' | grep -q '\"status\"'"
|
|
260
|
+
|
|
261
|
+
run_test "E2E: List runs" \
|
|
262
|
+
"curl -sf '$GATEWAY_URL/api/runs' \
|
|
263
|
+
-H 'x-api-key: $API_KEY' | grep -q '\"runs\"'"
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# Authentication Tests
|
|
267
|
+
echo ""
|
|
268
|
+
echo "--- Authentication Tests ---"
|
|
269
|
+
run_test "Auth: API key authentication required" \
|
|
270
|
+
"curl -sf '$GATEWAY_URL/api/runs' | grep -q '401\|403\|Unauthorized'"
|
|
271
|
+
|
|
272
|
+
run_test "Auth: Valid API key works" \
|
|
273
|
+
"curl -sf '$GATEWAY_URL/api/runs' \
|
|
274
|
+
-H 'x-api-key: $API_KEY' | grep -q '\"runs\"'"
|
|
275
|
+
|
|
276
|
+
run_test "Auth: Health endpoint doesn't require auth" \
|
|
277
|
+
"curl -sf '$GATEWAY_URL/health' | grep -q '\"status\"'"
|
|
278
|
+
|
|
279
|
+
# Rate Limiting Tests
|
|
280
|
+
echo ""
|
|
281
|
+
echo "--- Rate Limiting Tests ---"
|
|
282
|
+
run_test "Rate Limit: Health endpoint bypasses rate limiting" \
|
|
283
|
+
"for i in {1..20}; do curl -sf '$GATEWAY_URL/health' > /dev/null; done && true"
|
|
284
|
+
|
|
285
|
+
# Concurrent Operations
|
|
286
|
+
echo ""
|
|
287
|
+
echo "--- Concurrent Operations ---"
|
|
288
|
+
CONCURRENT_SUCCESS=0
|
|
289
|
+
for i in {1..5}; do
|
|
290
|
+
RESPONSE=$(curl -s --max-time 5 -X POST "$GATEWAY_URL/api/runs" \
|
|
291
|
+
-H "x-api-key: $API_KEY" \
|
|
292
|
+
-H "Content-Type: application/json" \
|
|
293
|
+
-d "{\"name\":\"Concurrent Test $i\",\"input\":{\"index\":$i}}" 2>/dev/null)
|
|
294
|
+
if [ ! -z "$RESPONSE" ] && (echo "$RESPONSE" | grep -q '"run"\|"success"'); then
|
|
295
|
+
CONCURRENT_SUCCESS=$((CONCURRENT_SUCCESS + 1))
|
|
296
|
+
fi
|
|
297
|
+
done
|
|
298
|
+
|
|
299
|
+
run_test "Concurrency: Multiple runs can be created concurrently" \
|
|
300
|
+
"[ $CONCURRENT_SUCCESS -ge 3 ]"
|
|
301
|
+
|
|
302
|
+
# Priority Queue
|
|
303
|
+
echo ""
|
|
304
|
+
echo "--- Priority Queue ---"
|
|
305
|
+
PRIORITY_RUN1_RESPONSE=$(curl -sf -X POST "$GATEWAY_URL/api/runs" \
|
|
306
|
+
-H "x-api-key: $API_KEY" \
|
|
307
|
+
-H "Content-Type: application/json" \
|
|
308
|
+
-d '{"name":"Normal Priority","input":{"priority":"normal"}}' 2>/dev/null)
|
|
309
|
+
|
|
310
|
+
PRIORITY_RUN1=$(echo "$PRIORITY_RUN1_RESPONSE" | grep -o '"run":{[^}]*"id":"[^"]*' | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
|
|
311
|
+
if [ -z "$PRIORITY_RUN1" ]; then
|
|
312
|
+
PRIORITY_RUN1=$(echo "$PRIORITY_RUN1_RESPONSE" | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
PRIORITY_RUN2_RESPONSE=$(curl -sf -X POST "$GATEWAY_URL/api/runs" \
|
|
316
|
+
-H "x-api-key: $API_KEY" \
|
|
317
|
+
-H "Content-Type: application/json" \
|
|
318
|
+
-d '{"name":"High Priority","input":{"priority":"high"}}' 2>/dev/null)
|
|
319
|
+
|
|
320
|
+
PRIORITY_RUN2=$(echo "$PRIORITY_RUN2_RESPONSE" | grep -o '"run":{[^}]*"id":"[^"]*' | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
|
|
321
|
+
if [ -z "$PRIORITY_RUN2" ]; then
|
|
322
|
+
PRIORITY_RUN2=$(echo "$PRIORITY_RUN2_RESPONSE" | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
if [ ! -z "$PRIORITY_RUN1" ] && [ ! -z "$PRIORITY_RUN2" ]; then
|
|
326
|
+
curl -sf -X POST "$GATEWAY_URL/api/runs/$PRIORITY_RUN1/start" \
|
|
327
|
+
-H "x-api-key: $API_KEY" \
|
|
328
|
+
-H "Content-Type: application/json" \
|
|
329
|
+
-d '{"priority":"normal"}' > /dev/null
|
|
330
|
+
|
|
331
|
+
curl -sf -X POST "$GATEWAY_URL/api/runs/$PRIORITY_RUN2/start" \
|
|
332
|
+
-H "x-api-key: $API_KEY" \
|
|
333
|
+
-H "Content-Type: application/json" \
|
|
334
|
+
-d '{"priority":"high"}' > /dev/null
|
|
335
|
+
|
|
336
|
+
sleep 2
|
|
337
|
+
|
|
338
|
+
run_test "Priority: Both runs started successfully" \
|
|
339
|
+
"curl -sf '$GATEWAY_URL/api/runs/$PRIORITY_RUN1' -H 'x-api-key: $API_KEY' | grep -q '\"status\"' && \
|
|
340
|
+
curl -sf '$GATEWAY_URL/api/runs/$PRIORITY_RUN2' -H 'x-api-key: $API_KEY' | grep -q '\"status\"'"
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
# Error Scenarios
|
|
344
|
+
echo ""
|
|
345
|
+
echo "--- Error Scenarios ---"
|
|
346
|
+
run_test "Errors: Invalid run ID returns 404" \
|
|
347
|
+
"curl -s '$GATEWAY_URL/api/runs/00000000-0000-0000-0000-000000000000' \
|
|
348
|
+
-H 'x-api-key: $API_KEY' | grep -q 'error'"
|
|
349
|
+
|
|
350
|
+
run_test "Errors: Invalid UUID format returns 400" \
|
|
351
|
+
"curl -s '$GATEWAY_URL/api/runs/invalid-id' \
|
|
352
|
+
-H 'x-api-key: $API_KEY' | grep -q 'error'"
|
|
353
|
+
|
|
354
|
+
run_test "Errors: Missing required field returns 400" \
|
|
355
|
+
"curl -s -X POST '$GATEWAY_URL/api/runs' \
|
|
356
|
+
-H 'x-api-key: $API_KEY' \
|
|
357
|
+
-H 'Content-Type: application/json' \
|
|
358
|
+
-d '{\"input\":{}}' | grep -q 'error'"
|
|
359
|
+
|
|
360
|
+
echo ""
|
|
361
|
+
echo "========================================="
|
|
362
|
+
echo "Test Summary"
|
|
363
|
+
echo "========================================="
|
|
364
|
+
echo ""
|
|
365
|
+
echo "Total Tests: $TOTAL_TESTS"
|
|
366
|
+
echo -e "${GREEN}Passed: $PASSED_TESTS${NC}"
|
|
367
|
+
echo -e "${RED}Failed: $FAILED_TESTS${NC}"
|
|
368
|
+
echo ""
|
|
369
|
+
|
|
370
|
+
if [ $FAILED_TESTS -gt 0 ]; then
|
|
371
|
+
echo -e "${RED}Failed Tests:${NC}"
|
|
372
|
+
for test_name in "${FAILED_TEST_NAMES[@]}"; do
|
|
373
|
+
echo " - $test_name"
|
|
374
|
+
done
|
|
375
|
+
echo ""
|
|
376
|
+
echo -e "${RED}❌ Some tests failed. Please review the errors above.${NC}"
|
|
377
|
+
exit 1
|
|
378
|
+
else
|
|
379
|
+
echo -e "${GREEN}✅ All tests passed!${NC}"
|
|
380
|
+
exit 0
|
|
381
|
+
fi
|
|
382
|
+
EOFTEST
|
|
383
|
+
|
|
384
|
+
chmod +x /opt/4runr/gateway/test-all-phases.sh
|
|
385
|
+
echo "✓ Clean test-all-phases.sh created successfully"
|
|
386
|
+
|