From 8a4c6b5d50c8ecd029937c1d2c7265de9a251980 Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Thu, 19 Feb 2026 20:20:05 -0800 Subject: [PATCH 01/11] feat: add Docker infrastructure for integration tests --- .dockerignore | 5 ++++ test/integration/Dockerfile | 37 +++++++++++++++++++++++++++++ test/integration/docker-compose.yml | 9 +++++++ vitest.integration.config.ts | 9 +++++++ 4 files changed, 60 insertions(+) create mode 100644 .dockerignore create mode 100644 test/integration/Dockerfile create mode 100644 test/integration/docker-compose.yml create mode 100644 vitest.integration.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..24c0348 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.git +dist +docs +.claude diff --git a/test/integration/Dockerfile b/test/integration/Dockerfile new file mode 100644 index 0000000..c7ff200 --- /dev/null +++ b/test/integration/Dockerfile @@ -0,0 +1,37 @@ +FROM node:22-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + curl \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# Install OpenClaw globally +RUN npm install -g openclaw@latest + +# Copy ClawRouter source and build +COPY . /opt/clawrouter +WORKDIR /opt/clawrouter +RUN npm ci && npm run build + +# Make package globally available +RUN npm link + +# Create non-root test user +RUN useradd -m -s /bin/bash testuser + +# Set up npm global prefix for testuser +USER testuser +RUN mkdir -p ~/.npm-global && \ + npm config set prefix ~/.npm-global +ENV PATH="/home/testuser/.npm-global/bin:${PATH}" + +# Link clawrouter into testuser's space +RUN npm link @blockrun/clawrouter + +# Create OpenClaw config directory +RUN mkdir -p ~/.openclaw/blockrun + +# Run integration tests via vitest +CMD ["npx", "--prefix", "/opt/clawrouter", "vitest", "run", "--config", "/opt/clawrouter/vitest.integration.config.ts"] diff --git a/test/integration/docker-compose.yml b/test/integration/docker-compose.yml new file mode 100644 index 0000000..d3c16ac --- /dev/null +++ b/test/integration/docker-compose.yml @@ -0,0 +1,9 @@ +services: + integration: + build: + context: ../.. + dockerfile: test/integration/Dockerfile + environment: + - BLOCKRUN_WALLET_KEY=${BLOCKRUN_WALLET_KEY:-} + - BLOCKRUN_PROXY_PORT=8402 + network_mode: host diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts new file mode 100644 index 0000000..84a23f2 --- /dev/null +++ b/vitest.integration.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/integration/**/*.test.ts"], + testTimeout: 30_000, + hookTimeout: 15_000, + }, +}); From 50f748edd41f97a91aba06ed7ae39aa504993d4a Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Thu, 19 Feb 2026 20:28:26 -0800 Subject: [PATCH 02/11] test: add integration test setup and test files for proxy lifecycle and full-flow --- test/integration/full-flow.test.ts | 97 ++++++++++++++++++++++++++++++ test/integration/lifecycle.test.ts | 76 +++++++++++++++++++++++ test/integration/setup.ts | 66 ++++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 test/integration/full-flow.test.ts create mode 100644 test/integration/lifecycle.test.ts create mode 100644 test/integration/setup.ts diff --git a/test/integration/full-flow.test.ts b/test/integration/full-flow.test.ts new file mode 100644 index 0000000..960a847 --- /dev/null +++ b/test/integration/full-flow.test.ts @@ -0,0 +1,97 @@ +/** + * Layer 2 — Full-flow integration tests (requires funded wallet). + * + * Gated on BLOCKRUN_WALLET_KEY env var. These tests make real API calls + * through the proxy to verify end-to-end chat completion flow. + */ + +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startTestProxy, stopTestProxy, getTestProxyUrl } from "./setup.js"; + +describe.skipIf(!process.env.BLOCKRUN_WALLET_KEY)( + "ClawRouter full-flow (funded wallet)", + () => { + beforeAll(async () => { + await startTestProxy(); + }); + + afterAll(async () => { + await stopTestProxy(); + }); + + it( + "chat completion with blockrun/free returns valid response", + async () => { + const res = await fetch(`${getTestProxyUrl()}/v1/chat/completions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "blockrun/free", + messages: [{ role: "user", content: "Say hello in one word." }], + max_tokens: 50, + }), + }); + + expect(res.status).toBe(200); + + const body = (await res.json()) as { + choices: Array<{ message: { content: string } }>; + }; + expect(body.choices.length).toBeGreaterThan(0); + expect(body.choices[0].message.content).toBeTruthy(); + }, + 60_000, + ); + + it( + "chat completion with blockrun/auto resolves to a model", + async () => { + const res = await fetch(`${getTestProxyUrl()}/v1/chat/completions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "blockrun/auto", + messages: [{ role: "user", content: "What is 2+2?" }], + max_tokens: 50, + }), + }); + + expect(res.status).toBe(200); + + const body = (await res.json()) as { + model: string; + choices: Array<{ message: { content: string } }>; + }; + expect(body.model).toBeTruthy(); + expect(body.choices.length).toBeGreaterThan(0); + }, + 60_000, + ); + + it( + "streaming chat completion returns SSE with data and [DONE]", + async () => { + const res = await fetch(`${getTestProxyUrl()}/v1/chat/completions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "blockrun/free", + messages: [{ role: "user", content: "Say hi." }], + max_tokens: 50, + stream: true, + }), + }); + + expect(res.status).toBe(200); + + const contentType = res.headers.get("content-type") ?? ""; + expect(contentType).toContain("text/event-stream"); + + const text = await res.text(); + expect(text).toContain("data: "); + expect(text).toContain("[DONE]"); + }, + 60_000, + ); + }, +); diff --git a/test/integration/lifecycle.test.ts b/test/integration/lifecycle.test.ts new file mode 100644 index 0000000..5bfdee5 --- /dev/null +++ b/test/integration/lifecycle.test.ts @@ -0,0 +1,76 @@ +/** + * Layer 1 — Lifecycle integration tests (no API keys required). + * + * Verifies proxy health, model listing, stats, and 404 handling. + * These tests always run regardless of wallet funding. + */ + +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startTestProxy, stopTestProxy, getTestProxyUrl } from "./setup.js"; + +describe("ClawRouter proxy lifecycle", () => { + beforeAll(async () => { + await startTestProxy(); + }); + + afterAll(async () => { + await stopTestProxy(); + }); + + it("GET /health returns 200 with status ok and valid wallet address", async () => { + const res = await fetch(`${getTestProxyUrl()}/health`); + expect(res.status).toBe(200); + + const body = (await res.json()) as { status: string; wallet: string }; + expect(body.status).toBe("ok"); + expect(body.wallet).toMatch(/^0x[0-9a-fA-F]{40}$/); + }); + + it("GET /health?full=true includes balance info", async () => { + const res = await fetch(`${getTestProxyUrl()}/health?full=true`); + expect(res.status).toBe(200); + + const body = (await res.json()) as Record; + expect(body.status).toBe("ok"); + + // Full health check includes either balance or balanceError + const hasBalanceInfo = "balance" in body || "balanceError" in body; + expect(hasBalanceInfo).toBe(true); + }); + + it("GET /v1/models returns model list with routing profiles", async () => { + const res = await fetch(`${getTestProxyUrl()}/v1/models`); + expect(res.status).toBe(200); + + const body = (await res.json()) as { + object: string; + data: Array<{ id: string; object: string }>; + }; + expect(body.object).toBe("list"); + expect(body.data.length).toBeGreaterThan(0); + + const modelIds = body.data.map((m) => m.id); + // Routing profile models are registered without "blockrun/" prefix in BLOCKRUN_MODELS + expect(modelIds).toContain("auto"); + expect(modelIds).toContain("eco"); + expect(modelIds).toContain("free"); + expect(modelIds).toContain("premium"); + }); + + it("GET /nonexistent returns 404", async () => { + const res = await fetch(`${getTestProxyUrl()}/nonexistent`); + expect(res.status).toBe(404); + + const body = (await res.json()) as { error: string }; + expect(body.error).toBe("Not found"); + }); + + it("GET /stats returns stats JSON", async () => { + const res = await fetch(`${getTestProxyUrl()}/stats`); + expect(res.status).toBe(200); + + const body = (await res.json()) as Record; + expect(typeof body).toBe("object"); + expect(body).not.toBeNull(); + }); +}); diff --git a/test/integration/setup.ts b/test/integration/setup.ts new file mode 100644 index 0000000..031fe01 --- /dev/null +++ b/test/integration/setup.ts @@ -0,0 +1,66 @@ +/** + * Integration test setup — programmatically starts ClawRouter proxy. + * + * Shared across all integration test files via beforeAll/afterAll. + * Starts the proxy on port 8402, waits for /health to return 200, + * and caches the handle so multiple test files share one instance. + */ + +import { startProxy } from "../../src/proxy.js"; +import { resolveOrGenerateWalletKey } from "../../src/auth.js"; +import type { ProxyHandle } from "../../src/proxy.js"; + +const TEST_PORT = 8402; +const HEALTH_POLL_INTERVAL_MS = 200; +const HEALTH_TIMEOUT_MS = 5_000; + +let proxyHandle: ProxyHandle | undefined; + +/** + * Start the test proxy on port 8402. + * Polls /health until it returns 200 (up to 5s), then returns the handle. + * Reuses an existing handle if already started. + */ +export async function startTestProxy(): Promise { + if (proxyHandle) return proxyHandle; + + const wallet = await resolveOrGenerateWalletKey(); + + proxyHandle = await startProxy({ + walletKey: wallet.key, + port: TEST_PORT, + skipBalanceCheck: true, + }); + + // Wait for /health to return 200 + const deadline = Date.now() + HEALTH_TIMEOUT_MS; + while (Date.now() < deadline) { + try { + const res = await fetch(`${proxyHandle.baseUrl}/health`); + if (res.ok) return proxyHandle; + } catch { + // proxy not ready yet + } + await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL_MS)); + } + + throw new Error(`Test proxy did not become healthy within ${HEALTH_TIMEOUT_MS}ms`); +} + +/** + * Stop the test proxy and clear the cached handle. + */ +export async function stopTestProxy(): Promise { + if (!proxyHandle) return; + await proxyHandle.close(); + proxyHandle = undefined; +} + +/** + * Get the base URL of the running test proxy (e.g. http://127.0.0.1:8402). + * Throws if the proxy has not been started. + */ +export function getTestProxyUrl(): string { + if (!proxyHandle) throw new Error("Test proxy not started — call startTestProxy() first"); + return proxyHandle.baseUrl; +} From bad27fab81d0e21e347f624a3013bc4d2c0f66fe Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Thu, 19 Feb 2026 21:16:30 -0800 Subject: [PATCH 03/11] fix: simplify Dockerfile and add test:docker:integration script --- package.json | 3 ++- test/integration/Dockerfile | 27 +++++++-------------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 19ae15f..ca959c3 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "test:resilience:full": "npm run test:resilience:errors && npm run test:resilience:lifecycle && npm run test:resilience:stability:full", "test:e2e:tool-ids": "tsx test/e2e-tool-id-sanitization.ts", "test:docker:install": "bash test/run-docker-test.sh", - "test:docker:edge-cases": "bash test/docker/run-edge-cases.sh" + "test:docker:edge-cases": "bash test/docker/run-edge-cases.sh", + "test:docker:integration": "docker compose -f test/integration/docker-compose.yml up --build --abort-on-container-exit --exit-code-from integration" }, "keywords": [ "llm", diff --git a/test/integration/Dockerfile b/test/integration/Dockerfile index c7ff200..a561fa4 100644 --- a/test/integration/Dockerfile +++ b/test/integration/Dockerfile @@ -7,31 +7,18 @@ RUN apt-get update && apt-get install -y \ jq \ && rm -rf /var/lib/apt/lists/* -# Install OpenClaw globally +# Install OpenClaw globally (peer dependency) RUN npm install -g openclaw@latest # Copy ClawRouter source and build -COPY . /opt/clawrouter WORKDIR /opt/clawrouter -RUN npm ci && npm run build - -# Make package globally available -RUN npm link - -# Create non-root test user -RUN useradd -m -s /bin/bash testuser - -# Set up npm global prefix for testuser -USER testuser -RUN mkdir -p ~/.npm-global && \ - npm config set prefix ~/.npm-global -ENV PATH="/home/testuser/.npm-global/bin:${PATH}" - -# Link clawrouter into testuser's space -RUN npm link @blockrun/clawrouter +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build # Create OpenClaw config directory RUN mkdir -p ~/.openclaw/blockrun -# Run integration tests via vitest -CMD ["npx", "--prefix", "/opt/clawrouter", "vitest", "run", "--config", "/opt/clawrouter/vitest.integration.config.ts"] +# Run integration tests +CMD ["npx", "vitest", "run", "--config", "vitest.integration.config.ts"] From 7d3291903d9250fb5bbd4142046f41507f849801 Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Fri, 20 Feb 2026 15:20:10 -0800 Subject: [PATCH 04/11] feat: add OpenClaw security scanner integration test --- test/integration/security-scanner.test.ts | 112 ++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 test/integration/security-scanner.test.ts diff --git a/test/integration/security-scanner.test.ts b/test/integration/security-scanner.test.ts new file mode 100644 index 0000000..57bdf7b --- /dev/null +++ b/test/integration/security-scanner.test.ts @@ -0,0 +1,112 @@ +/** + * OpenClaw security scanner integration tests. + * + * Runs OpenClaw's skill-scanner (the same one that fires during plugin install) + * against ClawRouter's built dist/ to catch regressions like process.env + * triggering env-harvesting warnings. + * + * The scanner is imported directly from the installed openclaw package. + */ + +import { describe, it, expect, beforeAll } from "vitest"; +import { readdirSync } from "node:fs"; +import { pathToFileURL } from "node:url"; + +interface ScanFinding { + ruleId: string; + severity: "critical" | "warn" | "info"; + file: string; + line: number; + message: string; + evidence: string; +} + +interface ScanSummary { + scannedFiles: number; + critical: number; + warn: number; + info: number; + findings: ScanFinding[]; +} + +type ScanFn = (dir: string) => Promise; + +describe("OpenClaw security scanner", () => { + let scanDirectoryWithSummary: ScanFn | undefined; + + beforeAll(async () => { + // Locate openclaw's skill-scanner chunk in its dist/ + const openclawDist = "/usr/local/lib/node_modules/openclaw/dist/"; + try { + const files = readdirSync(openclawDist); + const scannerFile = files.find((f) => f.startsWith("skill-scanner")); + if (!scannerFile) { + console.warn("[scanner] skill-scanner chunk not found in openclaw dist — skipping"); + return; + } + const mod = (await import(pathToFileURL(`${openclawDist}${scannerFile}`).href)) as Record< + string, + unknown + >; + // The scanner exports scanDirectoryWithSummary as a minified name + const fn = Object.values(mod).find((v) => typeof v === "function") as ScanFn | undefined; + if (fn) { + scanDirectoryWithSummary = fn; + } else { + console.warn("[scanner] No function export found in skill-scanner module — skipping"); + } + } catch (err) { + console.warn(`[scanner] Could not load openclaw scanner: ${err}`); + } + }); + + it("dist/ has zero critical findings (no env-harvesting)", async () => { + if (!scanDirectoryWithSummary) { + console.log("[scanner] Scanner not available — test passes vacuously"); + return; + } + + const result = await scanDirectoryWithSummary("/opt/clawrouter/dist"); + + console.log(`[scanner] Scanned ${result.scannedFiles} files`); + console.log(`[scanner] Results: ${result.critical} critical, ${result.warn} warn, ${result.info} info`); + + if (result.findings.length > 0) { + for (const f of result.findings) { + console.log(`[scanner] [${f.severity}] ${f.ruleId}: ${f.message}`); + console.log(`[scanner] ${f.file}:${f.line}`); + console.log(`[scanner] evidence: ${f.evidence}`); + } + } + + // No critical findings — this catches env-harvesting regressions + expect(result.critical).toBe(0); + + // Verify env-harvesting specifically is absent + const envHarvesting = result.findings.filter((f) => f.ruleId === "env-harvesting"); + expect(envHarvesting).toHaveLength(0); + }); + + it("dist/ has no unexpected warn-level findings", async () => { + if (!scanDirectoryWithSummary) { + console.log("[scanner] Scanner not available — test passes vacuously"); + return; + } + + const result = await scanDirectoryWithSummary("/opt/clawrouter/dist"); + + // potential-exfiltration is expected (wallet read + network send) + const unexpectedWarns = result.findings.filter( + (f) => f.severity === "warn" && f.ruleId !== "potential-exfiltration", + ); + + if (unexpectedWarns.length > 0) { + for (const f of unexpectedWarns) { + console.error(`[scanner] Unexpected warning: [${f.ruleId}] ${f.message}`); + console.error(`[scanner] ${f.file}:${f.line}`); + } + } + + expect(unexpectedWarns).toHaveLength(0); + }); +}); From e386d8e810304f88519bea33ed3d03d51496ba7f Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sat, 21 Feb 2026 13:58:57 -0800 Subject: [PATCH 05/11] feat: add integration tests GitHub Actions workflow --- .github/workflows/integration.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/integration.yml diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..c74d086 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,23 @@ +name: Integration Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + integration: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build integration test image + run: docker compose -f test/integration/docker-compose.yml build + + - name: Run lifecycle + scanner tests + run: > + docker compose -f test/integration/docker-compose.yml + up --abort-on-container-exit --exit-code-from integration From 90666e4efbfd4265c373b4bb43a5f94323774a80 Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sat, 21 Feb 2026 14:27:47 -0800 Subject: [PATCH 06/11] refactor: resolve scanner paths dynamically instead of hardcoding Docker paths --- test/integration/security-scanner.test.ts | 32 ++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/test/integration/security-scanner.test.ts b/test/integration/security-scanner.test.ts index 57bdf7b..8ca078f 100644 --- a/test/integration/security-scanner.test.ts +++ b/test/integration/security-scanner.test.ts @@ -9,7 +9,9 @@ */ import { describe, it, expect, beforeAll } from "vitest"; +import { execSync } from "node:child_process"; import { readdirSync } from "node:fs"; +import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; interface ScanFinding { @@ -31,12 +33,36 @@ interface ScanSummary { type ScanFn = (dir: string) => Promise; +/** Resolve openclaw dist dir — try global npm root first, fall back to Docker path. */ +function resolveOpenclawDist(): string { + try { + const globalRoot = execSync("npm root -g", { encoding: "utf-8" }).trim(); + return `${globalRoot}/openclaw/dist/`; + } catch { + return "/usr/local/lib/node_modules/openclaw/dist/"; + } +} + +/** Resolve ClawRouter dist dir — relative to this file, fall back to Docker path. */ +function resolveClawrouterDist(): string { + const local = resolve(__dirname, "../../dist"); + try { + readdirSync(local); + return local; + } catch { + return "/opt/clawrouter/dist"; + } +} + describe("OpenClaw security scanner", () => { let scanDirectoryWithSummary: ScanFn | undefined; + let distDir: string; beforeAll(async () => { + distDir = resolveClawrouterDist(); + // Locate openclaw's skill-scanner chunk in its dist/ - const openclawDist = "/usr/local/lib/node_modules/openclaw/dist/"; + const openclawDist = resolveOpenclawDist(); try { const files = readdirSync(openclawDist); const scannerFile = files.find((f) => f.startsWith("skill-scanner")); @@ -66,7 +92,7 @@ describe("OpenClaw security scanner", () => { return; } - const result = await scanDirectoryWithSummary("/opt/clawrouter/dist"); + const result = await scanDirectoryWithSummary(distDir); console.log(`[scanner] Scanned ${result.scannedFiles} files`); console.log(`[scanner] Results: ${result.critical} critical, ${result.warn} warn, ${result.info} info`); @@ -93,7 +119,7 @@ describe("OpenClaw security scanner", () => { return; } - const result = await scanDirectoryWithSummary("/opt/clawrouter/dist"); + const result = await scanDirectoryWithSummary(distDir); // potential-exfiltration is expected (wallet read + network send) const unexpectedWarns = result.findings.filter( From 29eff1f85cc763d5a3bd5ad46cdb8e6b55d58b09 Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sat, 21 Feb 2026 14:28:01 -0800 Subject: [PATCH 07/11] feat: add lifecycle and full-flow integration tests to CI workflow --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a76bc8..177d7f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,3 +34,12 @@ jobs: - name: Build run: npm run build + + - name: Integration tests (lifecycle) + run: npx vitest run --config vitest.integration.config.ts test/integration/lifecycle.test.ts + + - name: Integration tests (full-flow) + if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} + env: + BLOCKRUN_WALLET_KEY: ${{ secrets.BLOCKRUN_WALLET_KEY }} + run: npx vitest run --config vitest.integration.config.ts test/integration/full-flow.test.ts From c3d6be9414a6cfbb995c17571557621287c0df41 Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sat, 21 Feb 2026 14:28:10 -0800 Subject: [PATCH 08/11] refactor: slim integration workflow to scanner-only Docker job --- .github/workflows/integration.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index c74d086..dee4f59 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -1,4 +1,4 @@ -name: Integration Tests +name: Security Scanner on: push: @@ -8,16 +8,17 @@ on: workflow_dispatch: jobs: - integration: + scanner: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Build integration test image + - name: Build integration image run: docker compose -f test/integration/docker-compose.yml build - - name: Run lifecycle + scanner tests + - name: Run security scanner run: > - docker compose -f test/integration/docker-compose.yml - up --abort-on-container-exit --exit-code-from integration + docker compose -f test/integration/docker-compose.yml run --rm + integration npx vitest run --config vitest.integration.config.ts + test/integration/security-scanner.test.ts From dfd32ac9c3ad712c941c229179a3ffe1bdb6f01b Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sat, 21 Feb 2026 14:30:34 -0800 Subject: [PATCH 09/11] remove full flow integration --- .github/workflows/ci.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 177d7f7..2b069e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,9 +37,3 @@ jobs: - name: Integration tests (lifecycle) run: npx vitest run --config vitest.integration.config.ts test/integration/lifecycle.test.ts - - - name: Integration tests (full-flow) - if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} - env: - BLOCKRUN_WALLET_KEY: ${{ secrets.BLOCKRUN_WALLET_KEY }} - run: npx vitest run --config vitest.integration.config.ts test/integration/full-flow.test.ts From 2ae2611f632bce8328338dbe233bd1fa16d98d3e Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sat, 21 Feb 2026 14:44:01 -0800 Subject: [PATCH 10/11] fix: format integration test files with prettier --- test/integration/full-flow.test.ts | 137 ++++++++++------------ test/integration/security-scanner.test.ts | 4 +- 2 files changed, 64 insertions(+), 77 deletions(-) diff --git a/test/integration/full-flow.test.ts b/test/integration/full-flow.test.ts index 960a847..5dfc213 100644 --- a/test/integration/full-flow.test.ts +++ b/test/integration/full-flow.test.ts @@ -8,90 +8,75 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { startTestProxy, stopTestProxy, getTestProxyUrl } from "./setup.js"; -describe.skipIf(!process.env.BLOCKRUN_WALLET_KEY)( - "ClawRouter full-flow (funded wallet)", - () => { - beforeAll(async () => { - await startTestProxy(); - }); +describe.skipIf(!process.env.BLOCKRUN_WALLET_KEY)("ClawRouter full-flow (funded wallet)", () => { + beforeAll(async () => { + await startTestProxy(); + }); - afterAll(async () => { - await stopTestProxy(); - }); + afterAll(async () => { + await stopTestProxy(); + }); - it( - "chat completion with blockrun/free returns valid response", - async () => { - const res = await fetch(`${getTestProxyUrl()}/v1/chat/completions`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: "blockrun/free", - messages: [{ role: "user", content: "Say hello in one word." }], - max_tokens: 50, - }), - }); + it("chat completion with blockrun/free returns valid response", async () => { + const res = await fetch(`${getTestProxyUrl()}/v1/chat/completions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "blockrun/free", + messages: [{ role: "user", content: "Say hello in one word." }], + max_tokens: 50, + }), + }); - expect(res.status).toBe(200); + expect(res.status).toBe(200); - const body = (await res.json()) as { - choices: Array<{ message: { content: string } }>; - }; - expect(body.choices.length).toBeGreaterThan(0); - expect(body.choices[0].message.content).toBeTruthy(); - }, - 60_000, - ); + const body = (await res.json()) as { + choices: Array<{ message: { content: string } }>; + }; + expect(body.choices.length).toBeGreaterThan(0); + expect(body.choices[0].message.content).toBeTruthy(); + }, 60_000); - it( - "chat completion with blockrun/auto resolves to a model", - async () => { - const res = await fetch(`${getTestProxyUrl()}/v1/chat/completions`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: "blockrun/auto", - messages: [{ role: "user", content: "What is 2+2?" }], - max_tokens: 50, - }), - }); + it("chat completion with blockrun/auto resolves to a model", async () => { + const res = await fetch(`${getTestProxyUrl()}/v1/chat/completions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "blockrun/auto", + messages: [{ role: "user", content: "What is 2+2?" }], + max_tokens: 50, + }), + }); - expect(res.status).toBe(200); + expect(res.status).toBe(200); - const body = (await res.json()) as { - model: string; - choices: Array<{ message: { content: string } }>; - }; - expect(body.model).toBeTruthy(); - expect(body.choices.length).toBeGreaterThan(0); - }, - 60_000, - ); + const body = (await res.json()) as { + model: string; + choices: Array<{ message: { content: string } }>; + }; + expect(body.model).toBeTruthy(); + expect(body.choices.length).toBeGreaterThan(0); + }, 60_000); - it( - "streaming chat completion returns SSE with data and [DONE]", - async () => { - const res = await fetch(`${getTestProxyUrl()}/v1/chat/completions`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: "blockrun/free", - messages: [{ role: "user", content: "Say hi." }], - max_tokens: 50, - stream: true, - }), - }); + it("streaming chat completion returns SSE with data and [DONE]", async () => { + const res = await fetch(`${getTestProxyUrl()}/v1/chat/completions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "blockrun/free", + messages: [{ role: "user", content: "Say hi." }], + max_tokens: 50, + stream: true, + }), + }); - expect(res.status).toBe(200); + expect(res.status).toBe(200); - const contentType = res.headers.get("content-type") ?? ""; - expect(contentType).toContain("text/event-stream"); + const contentType = res.headers.get("content-type") ?? ""; + expect(contentType).toContain("text/event-stream"); - const text = await res.text(); - expect(text).toContain("data: "); - expect(text).toContain("[DONE]"); - }, - 60_000, - ); - }, -); + const text = await res.text(); + expect(text).toContain("data: "); + expect(text).toContain("[DONE]"); + }, 60_000); +}); diff --git a/test/integration/security-scanner.test.ts b/test/integration/security-scanner.test.ts index 8ca078f..9684ab7 100644 --- a/test/integration/security-scanner.test.ts +++ b/test/integration/security-scanner.test.ts @@ -95,7 +95,9 @@ describe("OpenClaw security scanner", () => { const result = await scanDirectoryWithSummary(distDir); console.log(`[scanner] Scanned ${result.scannedFiles} files`); - console.log(`[scanner] Results: ${result.critical} critical, ${result.warn} warn, ${result.info} info`); + console.log( + `[scanner] Results: ${result.critical} critical, ${result.warn} warn, ${result.info} info`, + ); if (result.findings.length > 0) { for (const f of result.findings) { From a682ea10f33a6bfa7ada15b8f903fecab90cd5d1 Mon Sep 17 00:00:00 2001 From: Arthur Chiu Date: Sat, 21 Feb 2026 14:48:24 -0800 Subject: [PATCH 11/11] fix: skip native addon compilation in Docker openclaw install --- test/integration/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration/Dockerfile b/test/integration/Dockerfile index a561fa4..be4f570 100644 --- a/test/integration/Dockerfile +++ b/test/integration/Dockerfile @@ -8,7 +8,8 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* # Install OpenClaw globally (peer dependency) -RUN npm install -g openclaw@latest +# --ignore-scripts avoids native compilation of optional deps like @discordjs/opus +RUN npm install -g --ignore-scripts openclaw@latest # Copy ClawRouter source and build WORKDIR /opt/clawrouter