test: add Docker-mode integration tests for gateway connectivity regressions
* fix: show all agents in command select, disable those without active session Previously the agent select in the OrchestrationBar only listed agents with a session_key set (agents.filter(a => a.session_key)), causing the dropdown to appear completely empty when agents exist but none have an active session. This was confusing — users thought no agents were registered. Fix: show all registered agents in the dropdown. Agents without an active session_key are shown as disabled with a '— no session' suffix and a tooltip explaining why they can't be messaged. The 'No agents registered' placeholder is shown only when the agents array is truly empty. The send button remains correctly disabled when no valid agent is selected. Fixes #321 * test: add gateway health history e2e + utility unit tests - Add tests/gateway-health-history.spec.ts: Playwright e2e tests for GET /api/gateways/health/history and POST /api/gateways/health covering auth guards, response shape validation, chronological ordering, and gateway name resolution. - Add src/app/api/gateways/health/health-utils.test.ts: 30 vitest unit tests for the pure utility functions in the health probe route: ipv4InCidr, isBlockedUrl (SSRF protection), buildGatewayProbeUrl, parseGatewayVersion, and hasOpenClaw32ToolsProfileRisk. All 30 unit tests pass. Covers the health history feature introduced in feat(gateways): add health history logging and timeline (#300). * test: add Docker-mode integration tests for gateway connectivity regressions Covers three post-fix regression scenarios: - 404 on gateway health (wizard used GET; endpoint is POST-only) — #334 - EROFS / build-phase DB eager init in Docker — #337 - Gateway connect resolves ws_url without OPENCLAW_HOME env var Adds tests/docker-mode.spec.ts with four describe blocks: 1. Gateway health endpoint contract (POST=200, GET=405, no-auth=401) 2. Lazy DB init on first runtime request (read + write paths) 3. Gateway connect without home env vars 4. Onboarding gateway-link step lifecycle
This commit is contained in:
parent
e2ddf09a72
commit
ae39b9b190
|
|
@ -0,0 +1,183 @@
|
||||||
|
/**
|
||||||
|
* Docker-mode integration tests
|
||||||
|
*
|
||||||
|
* Covers the three regressions fixed before this test was added:
|
||||||
|
* 1. 404 on gateway health check — onboarding wizard was using GET
|
||||||
|
* but the endpoint only exposes POST (#334)
|
||||||
|
* 2. EROFS / busy-init write errors — db.ts eager init at build time
|
||||||
|
* caused "read-only filesystem" failures in Docker (#337)
|
||||||
|
* 3. Missing OPENCLAW_HOME env — gateway-config path resolution relied
|
||||||
|
* on env vars that may not be set in a minimal Docker environment
|
||||||
|
*
|
||||||
|
* All tests run against the live server and require no Docker daemon.
|
||||||
|
* They validate the API contract that the Docker runtime depends on.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { API_KEY_HEADER } from './helpers'
|
||||||
|
|
||||||
|
// ─── 1. Gateway health endpoint accepts POST, not GET ────────────────────────
|
||||||
|
|
||||||
|
test.describe('Docker mode – gateway health check endpoint contract', () => {
|
||||||
|
test('POST /api/gateways/health returns 200 with results array', async ({ request }) => {
|
||||||
|
const res = await request.post('/api/gateways/health', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
})
|
||||||
|
expect(res.status()).toBe(200)
|
||||||
|
const body = await res.json()
|
||||||
|
expect(Array.isArray(body.results)).toBe(true)
|
||||||
|
expect(typeof body.probed_at).toBe('number')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('GET /api/gateways/health returns 405 (method not allowed)', async ({ request }) => {
|
||||||
|
const res = await request.get('/api/gateways/health', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
})
|
||||||
|
// Next.js returns 405 for unregistered methods on route handlers
|
||||||
|
expect(res.status()).toBe(405)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('POST /api/gateways/health requires auth', async ({ request }) => {
|
||||||
|
const res = await request.post('/api/gateways/health')
|
||||||
|
expect(res.status()).toBe(401)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── 2. Database init does not blow up on first request (EROFS guard) ────────
|
||||||
|
//
|
||||||
|
// In Docker the build phase happens with a read-only overlay FS.
|
||||||
|
// The fix guards module-level getDatabase() behind !isBuildPhase so the
|
||||||
|
// first runtime request triggers lazy init, not the build step.
|
||||||
|
// We verify this by making requests that exercise DB paths.
|
||||||
|
|
||||||
|
test.describe('Docker mode – lazy DB init on first request', () => {
|
||||||
|
test('GET /api/onboarding succeeds (verifies DB accessible at runtime)', async ({ request }) => {
|
||||||
|
const res = await request.get('/api/onboarding', { headers: API_KEY_HEADER })
|
||||||
|
expect(res.status()).toBe(200)
|
||||||
|
const body = await res.json()
|
||||||
|
expect(body).toHaveProperty('steps')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('POST /api/gateways returns 201 (verifies DB write works at runtime)', async ({ request }) => {
|
||||||
|
const name = `docker-mode-test-gw-${Date.now()}`
|
||||||
|
const res = await request.post('/api/gateways', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
host: 'http://gateway.internal:4443',
|
||||||
|
port: 18789,
|
||||||
|
token: 'docker-mode-token',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(res.status()).toBe(201)
|
||||||
|
const body = await res.json()
|
||||||
|
const id = body.gateway?.id as number
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await request.delete('/api/gateways', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
data: { id },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── 3. Gateway connect resolves without OPENCLAW_HOME in environment ────────
|
||||||
|
//
|
||||||
|
// The connect endpoint resolves ws_url from the stored gateway record,
|
||||||
|
// not from env vars. It must work when OPENCLAW_HOME / OPENCLAW_STATE_DIR
|
||||||
|
// are absent (plain Docker with no mounted .openclaw volume).
|
||||||
|
|
||||||
|
test.describe('Docker mode – gateway connect works without home env vars', () => {
|
||||||
|
const cleanup: number[] = []
|
||||||
|
|
||||||
|
test.afterEach(async ({ request }) => {
|
||||||
|
for (const id of cleanup.splice(0)) {
|
||||||
|
await request.delete('/api/gateways', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
data: { id },
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('POST /api/gateways/connect returns ws_url derived from stored host (no env dependency)', async ({ request }) => {
|
||||||
|
// Register a gateway that would only be reachable inside Docker
|
||||||
|
const createRes = await request.post('/api/gateways', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
data: {
|
||||||
|
name: `docker-mode-connect-${Date.now()}`,
|
||||||
|
host: 'https://openclaw-gateway:4443/sessions',
|
||||||
|
port: 18789,
|
||||||
|
token: 'docker-internal-token',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(createRes.status()).toBe(201)
|
||||||
|
const createBody = await createRes.json()
|
||||||
|
const gatewayId = createBody.gateway?.id as number
|
||||||
|
cleanup.push(gatewayId)
|
||||||
|
|
||||||
|
const connectRes = await request.post('/api/gateways/connect', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
data: { id: gatewayId },
|
||||||
|
})
|
||||||
|
expect(connectRes.status()).toBe(200)
|
||||||
|
const connectBody = await connectRes.json()
|
||||||
|
|
||||||
|
// ws_url is derived purely from stored host — no env vars needed
|
||||||
|
expect(connectBody.ws_url).toBe('wss://openclaw-gateway:4443')
|
||||||
|
expect(connectBody.token).toBe('docker-internal-token')
|
||||||
|
expect(connectBody.token_set).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('POST /api/gateways/connect returns 404 for unknown id (no crash on missing env)', async ({ request }) => {
|
||||||
|
const res = await request.post('/api/gateways/connect', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
data: { id: 999999 },
|
||||||
|
})
|
||||||
|
expect(res.status()).toBe(404)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── 4. Onboarding gateway-link step marks correctly ─────────────────────────
|
||||||
|
//
|
||||||
|
// The wizard's gateway-link step previously checked health via a broken GET.
|
||||||
|
// Verify the full onboarding lifecycle works, including the gateway step.
|
||||||
|
|
||||||
|
test.describe('Docker mode – onboarding gateway-link step', () => {
|
||||||
|
test.beforeEach(async ({ request }) => {
|
||||||
|
await request.post('/api/onboarding', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
data: { action: 'reset' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('gateway-link step can be completed via POST /api/onboarding', async ({ request }) => {
|
||||||
|
const res = await request.post('/api/onboarding', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
data: { action: 'complete_step', step: 'gateway-link' },
|
||||||
|
})
|
||||||
|
expect(res.status()).toBe(200)
|
||||||
|
const body = await res.json()
|
||||||
|
expect(body.ok).toBe(true)
|
||||||
|
expect(body.completedSteps).toContain('gateway-link')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('onboarding state still shows showOnboarding=false after all steps done', async ({ request }) => {
|
||||||
|
const steps = ['welcome', 'interface-mode', 'gateway-link', 'credentials']
|
||||||
|
for (const step of steps) {
|
||||||
|
await request.post('/api/onboarding', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
data: { action: 'complete_step', step },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await request.post('/api/onboarding', {
|
||||||
|
headers: API_KEY_HEADER,
|
||||||
|
data: { action: 'complete' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request.get('/api/onboarding', { headers: API_KEY_HEADER })
|
||||||
|
expect(res.status()).toBe(200)
|
||||||
|
const body = await res.json()
|
||||||
|
expect(body.showOnboarding).toBe(false)
|
||||||
|
expect(body.completed).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue