Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,22 @@ jobs:

- name: Build
run: npm run build

webui-e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Run Playwright E2E in Docker Compose
run: docker compose --profile e2e run --rm playwright-run

- name: Upload Playwright artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-e2e-report
path: |
e2e/playwright-report
e2e/test-results
if-no-files-found: ignore
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ node_modules/
capabilities/*/ui/dist/
modes/*/ui/dist/
agent-webui/dist/
e2e/node_modules/
e2e/playwright-report/
e2e/test-results/

# Docker volumes (if running outside compose)
data/
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,38 @@ Default endpoints:
docker compose up -d kernel
```

## Tests

### Kernel BVT

```bash
docker compose run --rm kernel-test sh -c "mkdir -p test-results && pytest -v tests -m bvt --junitxml=test-results/unit-pytest.xml"
```

### WebUI build

```bash
cd agent-webui
npm ci --include=dev --no-audit --no-fund
npm run build
```

### Browser E2E UI mode

```bash
docker compose --profile e2e up playwright
```

Then open `http://localhost:9323` to run and inspect tests in Playwright UI mode. The Playwright container runs against the Compose E2E WebUI service and stubs kernel API responses in the browser, so these smoke tests do not require Ollama responses or worker completion.

### Browser E2E headless mode

```bash
docker compose --profile e2e run --rm playwright-run
```

Headless mode is the same path used by CI.

## API Snapshot

Current kernel endpoints include:
Expand Down
2 changes: 1 addition & 1 deletion agent-webui/src/app/components/input-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function InputArea({
textareaRef.current.style.height = 'auto';
textareaRef.current.focus();
}
}, [resetSignal, getUsage]);
}, [resetSignal]);

const handleSend = () => {
const validationError = validateMessage ? validateMessage(message) : null;
Expand Down
3 changes: 3 additions & 0 deletions agent-webui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
server: {
allowedHosts: ['webui-e2e'],
},

// File types to support raw imports. Never add .css, .tsx, or .ts files to this.
assetsInclude: ['**/*.svg', '**/*.csv'],
Expand Down
53 changes: 53 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,57 @@ services:
- ./agent-webui:/app
- webui_node_modules:/app/node_modules

playwright:
image: mcr.microsoft.com/playwright:v1.55.0-noble
working_dir: /e2e
profiles:
- e2e
depends_on:
webui-e2e:
condition: service_healthy
environment:
- PLAYWRIGHT_BASE_URL=http://webui-e2e:3000
ports:
- "${HOST_PORT_PLAYWRIGHT_UI:-9323}:9323"
command: sh -c "npm install --no-audit --no-fund --package-lock=false && npx playwright test --ui --ui-host=0.0.0.0 --ui-port=9323"
volumes:
- ./e2e:/e2e
- e2e_node_modules:/e2e/node_modules

playwright-run:
image: mcr.microsoft.com/playwright:v1.55.0-noble
working_dir: /e2e
profiles:
- e2e
depends_on:
webui-e2e:
condition: service_healthy
environment:
- PLAYWRIGHT_BASE_URL=http://webui-e2e:3000
command: sh -c "npm install --no-audit --no-fund --package-lock=false && npm test"
volumes:
- ./e2e:/e2e
- e2e_node_modules:/e2e/node_modules

webui-e2e:
image: node:20-alpine
working_dir: /app
command: sh -c "npm ci --include=dev --no-audit --no-fund && npm run dev:docker"
profiles:
- e2e
environment:
- CHOKIDAR_USEPOLLING=true
- VITE_API_URL=http://localhost:5501
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"]
interval: 2s
timeout: 2s
retries: 30
start_period: 5s
volumes:
- ./agent-webui:/app
- webui_e2e_node_modules:/app/node_modules

# ollama:
# # Uncomment to run Ollama as a bundled container.
# # By default, Ollama is expected to run natively on the host (better
Expand All @@ -98,5 +149,7 @@ services:
# - ollama_data:/root/.ollama

volumes:
e2e_node_modules:
webui_node_modules:
webui_e2e_node_modules:
# ollama_data:
12 changes: 12 additions & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "aigentos-e2e",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "1.55.0"
}
}
23 changes: 23 additions & 0 deletions e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
testDir: "./tests",
timeout: 30_000,
expect: {
timeout: 10_000,
},
fullyParallel: true,
reporter: [["list"], ["html", { open: "never", outputFolder: "playwright-report" }]],
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:5500",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
outputDir: "test-results",
});
182 changes: 182 additions & 0 deletions e2e/tests/app.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { expect, Page, test } from "@playwright/test";

const now = "2026-05-08T12:00:00.000Z";

async function stubKernel(page: Page) {
const conversations = new Map<string, { id: string; title: string; last_message: string; updated_at: string; message_count: number }>();

await page.route("**/*", async (route) => {
const request = route.request();
const url = new URL(request.url());
const path = url.pathname;

if (request.resourceType() === "document" || url.port === "3000") {
return route.fallback();
}

const json = (body: unknown, status = 200) =>
route.fulfill({
status,
contentType: "application/json",
body: JSON.stringify(body),
});

if (path === "/health") {
return json({
status: "ok",
version: "0.3.0-e2e",
tenant_id: "default",
model: "e2e-model",
ollama_base_url: "http://ollama:11434",
embedding_base_url: "http://ollama:11434",
is_warm: true,
});
}

if (path === "/api/llm/warmup") {
return json({ ok: true, status: "already_warmed", latency_ms: 1, model: "e2e-model", warmed_at: now });
}

if (path === "/api/conversations" && request.method() === "GET") {
return json([...conversations.values()]);
}

if (path === "/api/conversations" && request.method() === "POST") {
const id = "conv-e2e";
conversations.set(id, {
id,
title: "E2E chat",
last_message: "No messages yet",
updated_at: now,
message_count: 0,
});
return json({ id, title: "E2E chat", updated_at: now, messages: [] }, 201);
}

if (path === "/api/chat" && request.method() === "POST") {
const payload = request.postDataJSON() as { message: string; conversation_id: string };
conversations.set(payload.conversation_id, {
id: payload.conversation_id,
title: "E2E chat",
last_message: payload.message,
updated_at: now,
message_count: 1,
});
return json({ conversation_id: payload.conversation_id, event_id: "event-e2e-user", accepted_at: now }, 202);
}

if (path.match(/^\/api\/conversations\/[^/]+\/events$/)) {
return json({
id: "conv-e2e",
title: "E2E chat",
updated_at: now,
events: [],
background_updates: [],
});
}

if (path.match(/^\/api\/conversations\/[^/]+\/stream$/)) {
return route.fulfill({
status: 200,
contentType: "text/event-stream",
body: "",
});
}

if (path === "/api/performance/recent") return json([]);
if (path === "/api/performance/summary") {
return json({
exchange_count: 0,
latency_min_ms: 0,
latency_max_ms: 0,
latency_avg_ms: 0,
tokens_day: { total_tokens: 0, prompt_tokens: 0, completion_tokens: 0, exchange_count: 0, avg_tokens_per_exchange: 0 },
tokens_week: { total_tokens: 0, prompt_tokens: 0, completion_tokens: 0, exchange_count: 0, avg_tokens_per_exchange: 0 },
tokens_month: { total_tokens: 0, prompt_tokens: 0, completion_tokens: 0, exchange_count: 0, avg_tokens_per_exchange: 0 },
tokens_all_time: { total_tokens: 0, prompt_tokens: 0, completion_tokens: 0, exchange_count: 0, avg_tokens_per_exchange: 0 },
});
}

if (path === "/api/prompts/system") {
return json({ agent_id: "atlas", prompt: "You are Atlas.", component_count: 1, profile_name: "Default", is_custom: false });
}
if (path === "/api/prompts/components" || path === "/api/prompts/orchestrator") return json([]);
if (path === "/api/prompts/profiles") return json([{ id: "default", name: "Default", is_active: true, is_default: true }]);
if (path === "/api/prompts/context-settings") {
return json({
max_context_tokens: 4096,
max_response_tokens: 1024,
compact_trigger_pct: 0.9,
compact_instructions: "",
memory_enabled: true,
updated_at: now,
});
}

if (path === "/api/debug/logs") return json([]);
if (path === "/api/memory/chunks") return json({ memory_enabled: true, chunks: [] });
if (path === "/api/imports") return json([]);
if (path === "/api/mcp/servers") return json([]);

if (path === "/api/admin/export") return json({ version: "0.3.0-e2e", model: "e2e-model", ollama_base_url: "mock", data: {} });
if (path === "/api/admin/delete-all-data") return json({ ok: true, deleted_at: now });
if (path === "/api/baseline/start") return json({ job_id: "baseline-e2e", status: "queued" }, 202);
if (path === "/api/baseline/status/baseline-e2e") {
return json({
job_id: "baseline-e2e",
status: "completed",
model: "e2e-model",
total_calls: 0,
completed_calls: 0,
current_step: null,
started_at: now,
updated_at: now,
completed_at: now,
duration_ms: 0,
events: [],
error: null,
result: { model: "e2e-model", mode: "direct_model", started_at: now, completed_at: now, duration_ms: 0, total_calls: 0, categories: [] },
});
}

return route.fulfill({ status: 404, contentType: "application/json", body: JSON.stringify({ detail: `Unhandled E2E route: ${path}` }) });
});
}

test.beforeEach(async ({ page }) => {
await stubKernel(page);
});

test("loads the chat shell and settings status", async ({ page }) => {
await page.goto("/");

await expect(page.getByRole("heading", { name: "Atlas" })).toBeVisible();
await expect(page.getByText("No conversations yet")).toBeVisible();
await expect(page.getByText("Model Ready (e2e-model)")).toBeVisible();

await page.getByRole("button", { name: "Settings" }).click();
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
await expect(page.getByText("Connected + Ready")).toBeVisible();
await expect(page.getByText("v0.3.0-e2e", { exact: true })).toBeVisible();
});

test("creates a conversation and queues a chat turn", async ({ page }) => {
await page.goto("/");

await page.getByPlaceholder("Message your agent...").fill("Hello from Playwright");
await page.getByRole("button", { name: "Send message" }).click();

await expect(page.getByText("Hello from Playwright")).toBeVisible();
await expect(page.getByText("Queued for worker...")).toBeVisible();
await expect(page.getByText("E2E chat")).toBeVisible();
});

test("opens the dashboard orchestration view", async ({ page }) => {
await page.goto("/");

await page.getByRole("button", { name: "Dashboard" }).click();

await expect(page.getByRole("heading", { name: "Orchestration", exact: true })).toBeVisible();
await expect(page.getByText("Behavior Layers")).toBeVisible();
await expect(page.getByText("v0.3.0-e2e", { exact: true })).toBeVisible();
});
Loading