Skip to content
Merged
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
98 changes: 63 additions & 35 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,80 @@ on:
pull_request:

jobs:
build:
name: build + smoke
stack-smoke:
name: compose + browser smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build image
uses: docker/build-push-action@v6
with:
context: .
push: false
load: true
tags: replaykit:ci
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Smoke test - collector starts
- name: Build compose stack
run: docker compose build

- name: Start compose stack
run: docker compose up -d

- name: Wait for collector /healthz
run: |
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:4100/healthz > /dev/null; then
echo "collector ready"; exit 0
fi
sleep 2
done
echo "collector did not become ready"; exit 1

- name: Wait for api /healthz
run: |
docker run --rm -d --name smoke-collector \
-e REPLAYKIT_PORT=4100 \
-e REPLAYKIT_DATA_ROOT=/data \
-p 4100:4100 \
replaykit:ci replaykit-collector
for i in $(seq 1 15); do
if curl -sf http://localhost:4100/healthz > /dev/null 2>&1; then
echo "Collector responded"
break
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:3210/healthz > /dev/null; then
echo "api ready"; exit 0
fi
sleep 1
sleep 2
done
docker stop smoke-collector
echo "api did not become ready"; exit 1

- name: Smoke test - API starts
- name: Wait for web /
run: |
docker run --rm -d --name smoke-api \
-p 3210:3210 \
replaykit:ci \
replaykit --storage sqlite --data-root /data serve --port 3210 --host 0.0.0.0
for i in $(seq 1 15); do
if curl -sf http://localhost:3210/healthz > /dev/null 2>&1; then
echo "API responded"
break
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:5173/ > /dev/null; then
echo "web ready"; exit 0
fi
sleep 1
sleep 2
done
docker stop smoke-api
echo "web did not become ready"; exit 1

- name: Seed one run through collector
env:
REPLAYKIT_COLLECTOR_URL: http://127.0.0.1:4100
run: bash scripts/seed-stack-run.sh

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: apps/web/package-lock.json

- name: Install web deps for Playwright
working-directory: apps/web
run: npm ci

- name: Install Playwright browser
working-directory: apps/web
run: npx playwright install --with-deps chromium

- name: Run Playwright smoke
working-directory: apps/web
env:
PLAYWRIGHT_BASE_URL: http://127.0.0.1:5173
run: npm run test:e2e

- name: Dump compose logs on failure
if: failure()
run: docker compose logs --no-color

- name: Tear down
if: always()
run: docker compose down -v
3 changes: 3 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ jobs:

- name: Test
run: cargo test --workspace

- name: Native collector and storage smoke
run: bash scripts/smoke-test.sh
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ data/
node_modules/
.env*
*.db
apps/web/.npm-cache/
apps/web/dist/
apps/web/test-results/
apps/web/playwright-report/
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ FROM debian:bookworm-slim

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*

COPY --from=build /app/target/release/replaykit /usr/local/bin/replaykit
COPY --from=build /app/target/release/replaykit-collector /usr/local/bin/replaykit-collector

Expand Down
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,21 @@ cargo run --bin replaykit -- --storage sqlite --data-root ./data demo-branch

## Docker Quick Start

The compose file starts the collector and API as separate services sharing a persistent volume:
The compose file starts the collector, API, and web app as separate services sharing a persistent volume:

```bash
docker compose up --build
```

- Collector: `http://localhost:4100`
- API: `http://localhost:3210`
- Web: `http://localhost:5173`

Seed one real run into the running stack:

```bash
bash scripts/seed-stack-run.sh
```

## Storage Layout

Expand Down Expand Up @@ -89,8 +96,16 @@ cargo test --workspace
# Web
cd apps/web && npm ci --cache .npm-cache && npm run lint && npm test && npm run build

# Smoke test (starts services, seeds data, verifies end-to-end)
# Browser smoke prerequisites
cd apps/web && npx playwright install chromium

# Backend smoke (starts real binaries, checks storage and restart persistence)
bash scripts/smoke-test.sh

# Native full stack: collector + SQLite + API + CLI fork/diff + live browser
# Install the web dependencies and Playwright Chromium first:
# cd apps/web && npm ci && npx playwright install chromium && cd ../..
python3 scripts/native-stack-gate.py
```

## Architecture Documentation
Expand Down Expand Up @@ -123,3 +138,11 @@ fixtures/
## License

MIT

The native stack gate uses frontend port 5173 because the API deliberately
allows the documented local development origins. It fails if that port is busy.
`PLAYWRIGHT_CHROMIUM_EXECUTABLE=/path/to/chromium` can select an existing browser.
Backend ports are selected dynamically, and `CARGO_TARGET_DIR` is respected.
The CLI demo fork intentionally retains its recorded failed LLM span; the gate
checks the real tool-output change and cached diff without claiming live-model
execution.
17 changes: 17 additions & 0 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM node:22-bookworm-slim

WORKDIR /app

COPY apps/web/package.json apps/web/package-lock.json ./
RUN npm ci

COPY apps/web/ ./

ARG VITE_REPLAYKIT_API_BASE_URL=http://localhost:3210
ENV VITE_REPLAYKIT_API_BASE_URL=$VITE_REPLAYKIT_API_BASE_URL

RUN npm run build

EXPOSE 5173

CMD ["npx", "vite", "preview", "--host", "0.0.0.0", "--port", "5173"]
37 changes: 37 additions & 0 deletions apps/web/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect, test } from '@playwright/test';

// Minimal smoke against the full Compose stack (collector + api + web).
// The test validates that:
// 1. the web container serves the bundled app,
// 2. the bundle boots in a real browser (no runtime errors),
// 3. a seeded run appears in live mode,
// 4. selecting that run loads the tree and artifact viewer.
//
// The stack is expected to be up before this test runs; CI brings it up via
// `docker compose up -d`, seeds one run via the collector, and pings healthz
// endpoints first.

test('web app boots and shows seeded live data', async ({ page }) => {
const pageErrors: Error[] = [];
page.on('pageerror', (err) => pageErrors.push(err));

await page.goto('/');
await expect(page).toHaveTitle(/ReplayKit/i);
await expect(page.getByRole('alert')).toHaveCount(0);
await expect(page.getByTestId('run-list')).toBeVisible();

const seededRun = page.getByText('smoke test run', { exact: true });
await expect(seededRun).toBeVisible();
await seededRun.click();

const treeNode = page.getByText('smoke-tool', { exact: true });
await expect(treeNode).toBeVisible();
await treeNode.click();

await expect(page.getByTestId('span-inspector')).toBeVisible();
await expect(page.getByTestId('artifact-viewer')).toBeVisible();
await expect(page.getByTestId('artifact-viewer')).toContainText('smoke test artifact');

expect(pageErrors, `unexpected runtime errors: ${pageErrors.map((e) => e.message).join(' | ')}`)
.toHaveLength(0);
});
2 changes: 1 addition & 1 deletion apps/web/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';

export default tseslint.config(
{ ignores: ['dist', 'node_modules', '.npm-cache'] },
{ ignores: ['dist', 'node_modules', '.npm-cache', 'e2e', 'playwright.config.ts', 'playwright-report', 'test-results'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
Expand Down
64 changes: 64 additions & 0 deletions apps/web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest"
"test:watch": "vitest",
"test:e2e": "playwright test"
},
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@playwright/test": "^1.48.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^24.12.2",
Expand Down
22 changes: 22 additions & 0 deletions apps/web/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
timeout: 30_000,
expect: { timeout: 10_000 },
forbidOnly: !!process.env.CI,
reporter: 'list',
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://127.0.0.1:5173',
trace: 'retain-on-failure',
launchOptions: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE
? { executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE }
: {},
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
Loading
Loading