Skip to content

Commit 918d68c

Browse files
authored
feat: Post-foundation features, UI, and test coverage (#89)
* feat: Add post-foundation features, tests, and UI enhancements - JWT-bound project scoping, capability-based task assignment, and RBAC simplification - DAG editor for campaign creation wizard with visual dependency management - Results page with search, pagination, and CSV export - Select-project page with auto-select for single-project users - Resource upload modal for wordlists, rulelists, and masklists - WebSocket event authentication with hybrid cookie/query-token support - Dashboard stats endpoint and real-time connection indicator - Playwright E2E suite with global setup/teardown and seed data - 113 frontend tests covering all domain pages, hooks, and components - 98 backend tests including events auth, heartbeat monitor, and contract tests - Fix 401 handling: global interceptor for session expiry, login uses 400 - Code splitting, connection pooling, missing DB indexes Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io> * fix: Replace Bun-specific APIs with Node.js in Playwright E2E setup Playwright runs under Node.js, not Bun, so Bun.spawn/Bun.sleep/import.meta.dir cause ReferenceError in CI. Replace with child_process.execFileSync/spawn, setTimeout-based sleep, and fileURLToPath for __dirname. Also add top-level permissions block to CI workflow per CodeQL finding. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io> * fix: Address Copilot and CodeRabbit review findings - Fix campaign rollback to restore startedAt/completedAt/progress on enqueue failure, not just status (CodeRabbit) - Fix agentMatchesCapabilities to handle hashcatMode vs hashModes array mismatch via array containment check (CodeRabbit) - Remove Zustand store dependency from useCreateResource and useUploadResourceFile cache invalidation — use partial key matching per TanStack Query pattern (CodeRabbit) - Parallelize inline task generation with Promise.all() (Copilot) - Revert CI permissions block that broke oven-sh/setup-bun token auth Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io> * fix: Fix E2E CI — use db:push for test schema, build shared before E2E - Use drizzle-kit push instead of migrate in E2E setup (no migration files needed for test databases) - Add db:push script to backend package.json - Add shared package build step before E2E tests in CI workflow (Vite needs @hashhive/shared built to resolve imports) Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io> * fix: Replace Bun.password.hash with subprocess call in E2E seed data Playwright runs under Node.js, not Bun. Delegate bcrypt hashing to a Bun subprocess via execFileSync since Bun is available on the CI runner. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io> * fix: Address CodeRabbit E2E infrastructure feedback - Add per-request AbortController timeout to waitForServer fetch calls - Move S3 client destroy to finally block to prevent leak on error - Use Promise.allSettled in teardown to avoid leaking containers Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io> * fix: Stabilize E2E smoke tests and address CodeRabbit feedback - Use waitUntil: 'networkidle' + waitForSelector in smoke tests to handle Vite HMR reloads during CI - Add per-request AbortController timeout to waitForServer fetch - Move S3 client destroy to finally block to prevent leak on error - Use Promise.allSettled in teardown to avoid leaking containers Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io> * fix: Replace non-null assertions with explicit guards in E2E seed data Aligns with noUncheckedIndexedAccess — throw descriptive errors instead of suppressing with ! operator. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io> * fix: Break 401 redirect loop on login page The API client's global 401 handler unconditionally redirected to /login, causing an infinite reload loop when fetchUser() fired on the login page. Skip the redirect when already on /login. Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io> --------- Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
1 parent 864be8f commit 918d68c

80 files changed

Lines changed: 6410 additions & 341 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,38 @@ jobs:
3333
uses: codecov/codecov-action@v5
3434
with:
3535
token: ${{ secrets.CODECOV_TOKEN }}
36+
37+
e2e-tests:
38+
runs-on: ubuntu-latest
39+
needs: ci-check
40+
steps:
41+
- uses: actions/checkout@v5
42+
with:
43+
fetch-depth: 0
44+
45+
- uses: oven-sh/setup-bun@v2
46+
with:
47+
bun-version-file: '.bun-version'
48+
49+
- uses: actions/setup-node@v5
50+
with:
51+
node-version-file: '.node-version'
52+
53+
- name: Install dependencies
54+
run: bun install
55+
56+
- name: Build shared package
57+
run: bun run build
58+
59+
- name: Install Playwright browsers
60+
run: bunx playwright install --with-deps chromium
61+
62+
- name: Run E2E tests
63+
run: bun --filter @hashhive/frontend test:e2e
64+
65+
- name: Upload Playwright report
66+
if: always()
67+
uses: actions/upload-artifact@v4
68+
with:
69+
name: playwright-report
70+
path: packages/frontend/playwright-report/

AGENTS.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ See `.kiro/steering/tech.md` for the full-stack details and `spec/epic/specs/Tec
102102

103103
Drizzle table definitions → drizzle-zod → Zod schemas → TypeScript types. One direction, no duplication.
104104

105+
When backend route validation changes (e.g., removing a field), update the corresponding shared Zod schema in `shared/src/schemas/index.ts` — the frontend imports these types via `@hashhive/shared`.
106+
105107
- **Drizzle tables** in `shared/src/db/schema.ts` define the database schema and generate migrations
106108
- **drizzle-zod** generates Zod schemas from Drizzle tables for API validation
107109
- **z.infer** derives TypeScript types from Zod schemas — no manually duplicated interfaces
@@ -114,6 +116,7 @@ The backend is a Bun + Hono + TypeScript service:
114116
- HTTP endpoints grouped by API surface: Agent API, Dashboard API
115117
- Thin handlers: parse/validate input with Zod, call Drizzle queries or service layer, return responses
116118
- **Services (`src/services/`)** — optional, only when route handlers become complex
119+
- **Circular import note:** `campaigns.ts` and `tasks.ts` have a circular dependency. Resolved via dynamic `await import('./tasks.js')` in campaigns.ts. Maintain this pattern when adding cross-service calls.
117120
- `AuthService`: login/logout, JWT/session management
118121
- `AgentService`: registration, capability detection, heartbeat handling
119122
- `CampaignService`: campaign lifecycle, DAG validation, attack configuration
@@ -124,6 +127,12 @@ The backend is a Bun + Hono + TypeScript service:
124127
- **Database (`src/db/`)** — Drizzle client setup and connection config
125128
- **Middleware (`src/middleware/`)** — auth, validation, error handling
126129

130+
### RBAC middleware
131+
132+
Two RBAC middleware variants in `src/middleware/rbac.ts`:
133+
- `requireProjectAccess()` / `requireRole()` — reads projectId from JWT context (`currentUser.projectId`); used by most dashboard routes
134+
- `requireParamProjectAccess()` / `requireParamProjectRole()` — reads projectId from URL param (`c.req.param('projectId')`); used by project management routes like `GET /projects/:projectId`
135+
127136
### API surfaces
128137

129138
Two API surfaces on the same Hono instance, backed by the same service and data layer:
@@ -135,6 +144,7 @@ Two API surfaces on the same Hono instance, backed by the same service and data
135144
- Core endpoints: `POST /agent/heartbeat`, `POST /agent/tasks/next`, `POST /agent/tasks/:id/report`
136145
- **Dashboard API (`/api/v1/dashboard/*`)**
137146
- JWT + HttpOnly session cookie authenticated REST API for the React frontend
147+
- Project scoping is JWT-bound: `projectId` is embedded in the session token and read from `c.get('currentUser').projectId` — frontend never sends projectId as a query param
138148
- Standard CRUD operations with Zod validation
139149
- Low traffic (1-3 concurrent users)
140150

@@ -156,13 +166,18 @@ The frontend is a Vite + React 19 SPA (no server components, no meta-framework):
156166
- **Identity & access**: `users`, `projects`, `project_users` (roles as text array)
157167
- **Agents & telemetry**: `operating_systems`, `agents`, `agent_errors`
158168
- **Campaign orchestration**: `campaigns`, `attacks` (with DAG dependencies), `tasks` (work ranges, progress, results)
169+
- Campaign lifecycle: `draft``running``paused` / `completed` / `cancelled`. Stop action returns to `draft` (not `completed`). Start requires ≥1 attack.
170+
- `hash_items` has unique constraint on `(hashListId, hashValue)` — use `onConflictDoUpdate` for crack result attribution
159171
- **Resources**: `hash_lists`, `hash_items`, `hash_types`, `word_lists`, `rule_lists`, `mask_lists`
160172

161173
## Development commands
162174

163175
Commands are run from the workspace root using Bun and Turborepo:
164176

165177
```bash
178+
# Workspace filters use package.json names — `@hashhive/shared`, `@hashhive/backend`, `@hashhive/frontend`
179+
# Shorthand like `bun --filter shared` may fail; use full name: `bun --filter @hashhive/shared build`
180+
166181
# Development
167182
bun dev # Start all services via Turborepo
168183
bun --filter backend dev # Start backend only
@@ -186,6 +201,12 @@ bun --filter backend db:migrate # Run migrations
186201
bun --filter backend db:studio # Open Drizzle Studio
187202
```
188203

204+
### Local CI check
205+
206+
```bash
207+
just ci-check # Runs: lint → format-check → type-check → build → test (no Docker needed)
208+
```
209+
189210
## Testing strategy
190211

191212
- **bun:test** for all tests (Bun's built-in test runner — not Jest, not Vitest)
@@ -229,6 +250,7 @@ The tsconfig.base.json enables maximum strictness. Key patterns:
229250
- **`noUncheckedIndexedAccess`**: All `arr[i]` returns `T | undefined` — guard with null check before use
230251
- **`noPropertyAccessFromIndexSignature`**: Use `obj['key']` bracket notation for index signatures
231252
- **Biome `useLiteralKeys: "off"`**: MUST stay off — conflicts with the TS setting above
253+
- **`z.preprocess` + React Hook Form**: `z.preprocess` widens input type to `unknown`, breaking `zodResolver` under strict mode. Define the form type as an explicit interface (not `z.infer`) and cast: `zodResolver(schema) as unknown as Resolver<FormType>`
232254

233255
## Hono error handling
234256

@@ -247,11 +269,23 @@ Without this, auth middleware 401 responses get swallowed into 500s.
247269

248270
- Backend contract tests validate auth (401) and validation (400) without a running DB
249271
- Drizzle mock chains must match production code — e.g. `insert().values()` returning `{ onConflictDoNothing: mock() }`
272+
- BullMQ worker test mocks: if worker does `db.select()`, mock must return chainable `{ from: mock(() => chain), where: mock(() => Promise.resolve([])) }`
250273
- Frontend tests use `happy-dom` with manual global injection (not `@happy-dom/global-registrator`)
251274
- Always call `afterEach(cleanup)` in Testing Library tests — DOM persists in happy-dom
252275
- Test fixtures: `packages/backend/tests/fixtures.ts` — factory functions + token helpers
253276
- Biome overrides: `**/scripts/**` disables `noConsole` and `noExplicitAny` for CLI tools
254277

278+
### Frontend test utilities
279+
280+
- `tests/mocks/fetch.ts``mockFetch()` replaces global fetch with route-to-response mapping; call `restoreFetch()` in afterEach
281+
- `tests/mocks/websocket.ts``installMockWebSocket()` replaces global WebSocket; provides `simulateOpen/Close/Message`
282+
- `tests/fixtures/api-responses.ts` — factory functions: `mockLoginResponse`, `mockMeResponse`, `mockDashboardStats`
283+
- `tests/utils/store-reset.ts``resetAllStores()` resets all Zustand stores; call in afterEach
284+
- `tests/test-utils.tsx``renderWithProviders()` (single component), `renderWithRouter()` (navigation tests), `cleanupAll()` (DOM + stores)
285+
- **401 gotcha**: `api.ts` globally intercepts all 401 responses as "Session expired" — login tests must use 400 for invalid credentials
286+
- `@testing-library/user-event` is NOT installed — use `fireEvent` from `@testing-library/react`
287+
- **Run tests per-package**: Use `bun --filter @hashhive/frontend test` / `bun --filter @hashhive/backend test` — root `bun test` skips per-package `bunfig.toml` (happy-dom), causing `document is not defined`
288+
255289
## AI agent notes
256290

257291
- `.kiro/steering/` and `.kiro/specs/` are authoritative — always align structural changes with those documents rather than inferring architecture solely from current code.

0 commit comments

Comments
 (0)