Spaces:
Sleeping
fix: resolve all 44 failing CI E2E tests (#64)
Browse files* fix: resolve all 44 failing CI E2E tests
- Bypass non-critical rate limiters in test env (MC_DISABLE_RATE_LIMIT=1)
to prevent 429s when 165 tests share the same IP bucket
- Make admin seed idempotent (INSERT OR IGNORE) to fix UNIQUE constraint
race when multiple Next.js workers initialize concurrently
- Add distinct x-forwarded-for headers to login-flow tests so they never
share the critical login rate-limit bucket with other test suites
- Add missing 018_token_usage migration that the heartbeat POST handler
depends on, fixing the 500 on inline token reporting
* docs: update README with latest features and test count
- Update migration count from 15 to 18
- Update E2E test count from 146 to 165
- Move Direct CLI, OpenAPI docs, and GitHub sync to completed roadmap
- Add Direct CLI and GitHub sync feature descriptions
- Add /api/connect and /api/github to API reference
- Remove resolved known limitation (vitest stubs)
- Update repo description
* fix: prevent build-time admin seed with wrong credentials in CI
Move `cp .env.test .env` before `pnpm build` in CI workflow so env vars
are present during build. Add NEXT_PHASE guard to skip seed during build
as belt-and-suspenders β env vars may not be available at build time.
Root cause: `next build` imports db.ts, triggering seedAdminUserFromEnv()
with undefined AUTH_USER/AUTH_PASS, seeding user `admin` instead of
`testadmin`. Runtime seed then sees count > 0 and skips. Tests login
as `testadmin` which doesn't exist β 401.
- .env.test +1 -0
- .github/workflows/quality-gate.yml +3 -3
- README.md +24 -5
- src/lib/db.ts +4 -1
- src/lib/migrations.ts +18 -0
- tests/login-flow.spec.ts +5 -3
|
@@ -5,3 +5,4 @@ AUTH_SECRET=test-legacy-secret
|
|
| 5 |
MC_ALLOW_ANY_HOST=1
|
| 6 |
MC_COOKIE_SECURE=
|
| 7 |
MC_COOKIE_SAMESITE=lax
|
|
|
|
|
|
| 5 |
MC_ALLOW_ANY_HOST=1
|
| 6 |
MC_COOKIE_SECURE=
|
| 7 |
MC_COOKIE_SAMESITE=lax
|
| 8 |
+
MC_DISABLE_RATE_LIMIT=1
|
|
@@ -39,12 +39,12 @@ jobs:
|
|
| 39 |
- name: Unit tests
|
| 40 |
run: pnpm test
|
| 41 |
|
| 42 |
-
- name: Build
|
| 43 |
-
run: pnpm build
|
| 44 |
-
|
| 45 |
- name: Prepare E2E environment
|
| 46 |
run: cp .env.test .env
|
| 47 |
|
|
|
|
|
|
|
|
|
|
| 48 |
- name: Install Playwright browsers
|
| 49 |
run: pnpm exec playwright install --with-deps
|
| 50 |
|
|
|
|
| 39 |
- name: Unit tests
|
| 40 |
run: pnpm test
|
| 41 |
|
|
|
|
|
|
|
|
|
|
| 42 |
- name: Prepare E2E environment
|
| 43 |
run: cp .env.test .env
|
| 44 |
|
| 45 |
+
- name: Build
|
| 46 |
+
run: pnpm build
|
| 47 |
+
|
| 48 |
- name: Install Playwright browsers
|
| 49 |
run: pnpm exec playwright install --with-deps
|
| 50 |
|
|
@@ -61,7 +61,6 @@ Initial login is seeded from `AUTH_USER` / `AUTH_PASS` on first run.
|
|
| 61 |
### Known Limitations
|
| 62 |
|
| 63 |
- **CSP still includes `unsafe-inline`** β `unsafe-eval` has been removed, but inline styles remain for framework compatibility
|
| 64 |
-
- **Vitest stubs need real assertions** β unit test files exist but most are placeholder stubs
|
| 65 |
|
| 66 |
### Security Considerations
|
| 67 |
|
|
@@ -87,6 +86,12 @@ Token usage dashboard with per-model breakdowns, trend charts, and cost analysis
|
|
| 87 |
### Background Automation
|
| 88 |
Scheduled tasks for database backups, stale record cleanup, and agent heartbeat monitoring. Configurable via UI or API.
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
### Integrations
|
| 91 |
Outbound webhooks with delivery history, configurable alert rules with cooldowns, and multi-gateway connection management. Optional 1Password CLI integration for secret management.
|
| 92 |
|
|
@@ -108,7 +113,7 @@ mission-control/
|
|
| 108 |
β βββ lib/
|
| 109 |
β β βββ auth.ts # Session + API key auth, RBAC
|
| 110 |
β β βββ db.ts # SQLite (better-sqlite3, WAL mode)
|
| 111 |
-
β β βββ migrations.ts #
|
| 112 |
β β βββ scheduler.ts # Background task scheduler
|
| 113 |
β β βββ webhooks.ts # Outbound webhook delivery
|
| 114 |
β β βββ websocket.ts # Gateway WebSocket client
|
|
@@ -128,7 +133,7 @@ mission-control/
|
|
| 128 |
| Charts | Recharts 3 |
|
| 129 |
| Real-time | WebSocket + Server-Sent Events |
|
| 130 |
| Auth | scrypt hashing, session tokens, RBAC |
|
| 131 |
-
| Testing | Vitest + Playwright (
|
| 132 |
|
| 133 |
## Authentication
|
| 134 |
|
|
@@ -233,6 +238,18 @@ All endpoints require authentication unless noted. Full reference below.
|
|
| 233 |
| `GET/POST/PUT/DELETE` | `/api/alerts` | admin | Alert rules |
|
| 234 |
| `GET/POST/PUT/DELETE` | `/api/gateways` | admin | Gateway connections |
|
| 235 |
| `GET/PUT/DELETE/POST` | `/api/integrations` | admin | Integration management |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
</details>
|
| 238 |
|
|
@@ -339,13 +356,15 @@ See [open issues](https://github.com/builderz-labs/mission-control/issues) for p
|
|
| 339 |
- [x] Export endpoint row limits ([#43](https://github.com/builderz-labs/mission-control/issues/43))
|
| 340 |
- [x] Fill in Vitest unit test stubs with real assertions
|
| 341 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
**Up next:**
|
| 343 |
|
| 344 |
- [ ] Agent-agnostic gateway support β connect any orchestration framework (OpenClaw, ZeroClaw, OpenFang, NeoBot, IronClaw, etc.), not just OpenClaw
|
| 345 |
-
- [ ] Direct CLI integration β connect tools like Codex, Claude Code, or custom CLIs directly without requiring a gateway
|
| 346 |
- [ ] Native macOS app (Electron or Tauri)
|
| 347 |
- [ ] First-class per-agent cost breakdowns β dedicated panel with per-agent token usage and spend (currently derivable from per-session data)
|
| 348 |
-
- [ ] OpenAPI / Swagger documentation
|
| 349 |
- [ ] Webhook retry with exponential backoff
|
| 350 |
- [ ] OAuth approval UI improvements
|
| 351 |
- [ ] API token rotation UI
|
|
|
|
| 61 |
### Known Limitations
|
| 62 |
|
| 63 |
- **CSP still includes `unsafe-inline`** β `unsafe-eval` has been removed, but inline styles remain for framework compatibility
|
|
|
|
| 64 |
|
| 65 |
### Security Considerations
|
| 66 |
|
|
|
|
| 86 |
### Background Automation
|
| 87 |
Scheduled tasks for database backups, stale record cleanup, and agent heartbeat monitoring. Configurable via UI or API.
|
| 88 |
|
| 89 |
+
### Direct CLI Integration
|
| 90 |
+
Connect Claude Code, Codex, or any CLI tool directly to Mission Control without requiring a gateway. Register connections, send heartbeats with inline token reporting, and auto-register agents.
|
| 91 |
+
|
| 92 |
+
### GitHub Issues Sync
|
| 93 |
+
Inbound sync from GitHub repositories with label and assignee mapping. Synced issues appear on the task board alongside agent-created tasks.
|
| 94 |
+
|
| 95 |
### Integrations
|
| 96 |
Outbound webhooks with delivery history, configurable alert rules with cooldowns, and multi-gateway connection management. Optional 1Password CLI integration for secret management.
|
| 97 |
|
|
|
|
| 113 |
β βββ lib/
|
| 114 |
β β βββ auth.ts # Session + API key auth, RBAC
|
| 115 |
β β βββ db.ts # SQLite (better-sqlite3, WAL mode)
|
| 116 |
+
β β βββ migrations.ts # 18 schema migrations
|
| 117 |
β β βββ scheduler.ts # Background task scheduler
|
| 118 |
β β βββ webhooks.ts # Outbound webhook delivery
|
| 119 |
β β βββ websocket.ts # Gateway WebSocket client
|
|
|
|
| 133 |
| Charts | Recharts 3 |
|
| 134 |
| Real-time | WebSocket + Server-Sent Events |
|
| 135 |
| Auth | scrypt hashing, session tokens, RBAC |
|
| 136 |
+
| Testing | Vitest + Playwright (165 E2E tests) |
|
| 137 |
|
| 138 |
## Authentication
|
| 139 |
|
|
|
|
| 238 |
| `GET/POST/PUT/DELETE` | `/api/alerts` | admin | Alert rules |
|
| 239 |
| `GET/POST/PUT/DELETE` | `/api/gateways` | admin | Gateway connections |
|
| 240 |
| `GET/PUT/DELETE/POST` | `/api/integrations` | admin | Integration management |
|
| 241 |
+
| `POST` | `/api/github` | admin | Trigger GitHub Issues sync |
|
| 242 |
+
|
| 243 |
+
</details>
|
| 244 |
+
|
| 245 |
+
<details>
|
| 246 |
+
<summary><strong>Direct CLI</strong></summary>
|
| 247 |
+
|
| 248 |
+
| Method | Path | Role | Description |
|
| 249 |
+
|--------|------|------|-------------|
|
| 250 |
+
| `POST` | `/api/connect` | operator | Register direct CLI connection |
|
| 251 |
+
| `GET` | `/api/connect` | viewer | List active connections |
|
| 252 |
+
| `DELETE` | `/api/connect` | operator | Disconnect CLI session |
|
| 253 |
|
| 254 |
</details>
|
| 255 |
|
|
|
|
| 356 |
- [x] Export endpoint row limits ([#43](https://github.com/builderz-labs/mission-control/issues/43))
|
| 357 |
- [x] Fill in Vitest unit test stubs with real assertions
|
| 358 |
|
| 359 |
+
- [x] Direct CLI integration β connect tools like Codex, Claude Code, or custom CLIs directly without requiring a gateway ([#61](https://github.com/builderz-labs/mission-control/pull/61))
|
| 360 |
+
- [x] OpenAPI 3.1 documentation with Scalar UI ([#60](https://github.com/builderz-labs/mission-control/pull/60))
|
| 361 |
+
- [x] GitHub Issues sync β inbound sync with label/assignee mapping ([#63](https://github.com/builderz-labs/mission-control/pull/63))
|
| 362 |
+
|
| 363 |
**Up next:**
|
| 364 |
|
| 365 |
- [ ] Agent-agnostic gateway support β connect any orchestration framework (OpenClaw, ZeroClaw, OpenFang, NeoBot, IronClaw, etc.), not just OpenClaw
|
|
|
|
| 366 |
- [ ] Native macOS app (Electron or Tauri)
|
| 367 |
- [ ] First-class per-agent cost breakdowns β dedicated panel with per-agent token usage and spend (currently derivable from per-session data)
|
|
|
|
| 368 |
- [ ] Webhook retry with exponential backoff
|
| 369 |
- [ ] OAuth approval UI improvements
|
| 370 |
- [ ] API token rotation UI
|
|
@@ -74,6 +74,9 @@ function initializeSchema() {
|
|
| 74 |
interface CountRow { count: number }
|
| 75 |
|
| 76 |
function seedAdminUserFromEnv(dbConn: Database.Database): void {
|
|
|
|
|
|
|
|
|
|
| 77 |
const count = (dbConn.prepare('SELECT COUNT(*) as count FROM users').get() as CountRow).count
|
| 78 |
if (count > 0) return
|
| 79 |
|
|
@@ -82,7 +85,7 @@ function seedAdminUserFromEnv(dbConn: Database.Database): void {
|
|
| 82 |
const displayName = username.charAt(0).toUpperCase() + username.slice(1)
|
| 83 |
|
| 84 |
dbConn.prepare(`
|
| 85 |
-
INSERT INTO users (username, display_name, password_hash, role)
|
| 86 |
VALUES (?, ?, ?, ?)
|
| 87 |
`).run(username, displayName, hashPassword(password), 'admin')
|
| 88 |
|
|
|
|
| 74 |
interface CountRow { count: number }
|
| 75 |
|
| 76 |
function seedAdminUserFromEnv(dbConn: Database.Database): void {
|
| 77 |
+
// Skip seeding during `next build` β env vars may not be available yet
|
| 78 |
+
if (process.env.NEXT_PHASE === 'phase-production-build') return
|
| 79 |
+
|
| 80 |
const count = (dbConn.prepare('SELECT COUNT(*) as count FROM users').get() as CountRow).count
|
| 81 |
if (count > 0) return
|
| 82 |
|
|
|
|
| 85 |
const displayName = username.charAt(0).toUpperCase() + username.slice(1)
|
| 86 |
|
| 87 |
dbConn.prepare(`
|
| 88 |
+
INSERT OR IGNORE INTO users (username, display_name, password_hash, role)
|
| 89 |
VALUES (?, ?, ?, ?)
|
| 90 |
`).run(username, displayName, hashPassword(password), 'admin')
|
| 91 |
|
|
@@ -477,6 +477,24 @@ const migrations: Migration[] = [
|
|
| 477 |
CREATE INDEX IF NOT EXISTS idx_github_syncs_created_at ON github_syncs(created_at);
|
| 478 |
`)
|
| 479 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
}
|
| 481 |
]
|
| 482 |
|
|
|
|
| 477 |
CREATE INDEX IF NOT EXISTS idx_github_syncs_created_at ON github_syncs(created_at);
|
| 478 |
`)
|
| 479 |
}
|
| 480 |
+
},
|
| 481 |
+
{
|
| 482 |
+
id: '018_token_usage',
|
| 483 |
+
up: (db) => {
|
| 484 |
+
db.exec(`
|
| 485 |
+
CREATE TABLE IF NOT EXISTS token_usage (
|
| 486 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 487 |
+
model TEXT NOT NULL,
|
| 488 |
+
session_id TEXT NOT NULL,
|
| 489 |
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
| 490 |
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
| 491 |
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
| 492 |
+
);
|
| 493 |
+
CREATE INDEX IF NOT EXISTS idx_token_usage_session_id ON token_usage(session_id);
|
| 494 |
+
CREATE INDEX IF NOT EXISTS idx_token_usage_created_at ON token_usage(created_at);
|
| 495 |
+
CREATE INDEX IF NOT EXISTS idx_token_usage_model ON token_usage(model);
|
| 496 |
+
`)
|
| 497 |
+
}
|
| 498 |
}
|
| 499 |
]
|
| 500 |
|
|
@@ -18,7 +18,8 @@ test.describe('Login Flow', () => {
|
|
| 18 |
|
| 19 |
test('login API returns session cookie on success', async ({ request }) => {
|
| 20 |
const res = await request.post('/api/auth/login', {
|
| 21 |
-
data: { username: 'testadmin', password: 'testpass123' }
|
|
|
|
| 22 |
})
|
| 23 |
expect(res.status()).toBe(200)
|
| 24 |
|
|
@@ -38,7 +39,8 @@ test.describe('Login Flow', () => {
|
|
| 38 |
test('session cookie grants API access', async ({ request }) => {
|
| 39 |
// Login to get a session
|
| 40 |
const loginRes = await request.post('/api/auth/login', {
|
| 41 |
-
data: { username: 'testadmin', password: 'testpass123' }
|
|
|
|
| 42 |
})
|
| 43 |
expect(loginRes.status()).toBe(200)
|
| 44 |
|
|
@@ -50,7 +52,7 @@ test.describe('Login Flow', () => {
|
|
| 50 |
|
| 51 |
// Use the session cookie to access /api/auth/me
|
| 52 |
const meRes = await request.get('/api/auth/me', {
|
| 53 |
-
headers: { 'cookie': `mc-session=${sessionToken}` }
|
| 54 |
})
|
| 55 |
expect(meRes.status()).toBe(200)
|
| 56 |
const body = await meRes.json()
|
|
|
|
| 18 |
|
| 19 |
test('login API returns session cookie on success', async ({ request }) => {
|
| 20 |
const res = await request.post('/api/auth/login', {
|
| 21 |
+
data: { username: 'testadmin', password: 'testpass123' },
|
| 22 |
+
headers: { 'x-forwarded-for': '10.88.88.1' }
|
| 23 |
})
|
| 24 |
expect(res.status()).toBe(200)
|
| 25 |
|
|
|
|
| 39 |
test('session cookie grants API access', async ({ request }) => {
|
| 40 |
// Login to get a session
|
| 41 |
const loginRes = await request.post('/api/auth/login', {
|
| 42 |
+
data: { username: 'testadmin', password: 'testpass123' },
|
| 43 |
+
headers: { 'x-forwarded-for': '10.88.88.2' }
|
| 44 |
})
|
| 45 |
expect(loginRes.status()).toBe(200)
|
| 46 |
|
|
|
|
| 52 |
|
| 53 |
// Use the session cookie to access /api/auth/me
|
| 54 |
const meRes = await request.get('/api/auth/me', {
|
| 55 |
+
headers: { 'cookie': `mc-session=${sessionToken}`, 'x-forwarded-for': '10.88.88.2' }
|
| 56 |
})
|
| 57 |
expect(meRes.status()).toBe(200)
|
| 58 |
const body = await meRes.json()
|