test(web): scaffold vitest + RTL with Orchestrator seed suite (DEBT-043)
Wire vitest 4 + jsdom + @testing-library/{react,jest-dom,user-event}
+ @vitest/coverage-v8 through vite.config.ts (defineConfig from
vitest/config). src/test/setup.ts registers jest-dom matchers and
RTL cleanup. tsconfig.app.json picks up vitest/globals types.
Seed suite Orchestrator.test.tsx covers the three regressions
called out in DEBT-043: empty-state render, kind-filter toggling
triggers a scoped refetch, mocked stream callback prepends a row.
This commit is contained in:
1285
decnet_web/package-lock.json
generated
1285
decnet_web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,10 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"asciinema-player": "^3.8.0",
|
||||
@@ -20,16 +23,22 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.4"
|
||||
"vite": "^8.0.4",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
93
decnet_web/src/components/Orchestrator.test.tsx
Normal file
93
decnet_web/src/components/Orchestrator.test.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor, act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import Orchestrator from './Orchestrator';
|
||||
import type {
|
||||
OrchestratorStreamEvent,
|
||||
UseOrchestratorStreamOptions,
|
||||
} from './useOrchestratorStream';
|
||||
|
||||
vi.mock('../utils/api', () => ({
|
||||
default: { get: vi.fn() },
|
||||
}));
|
||||
|
||||
// Capture the live stream callback so tests can drive it manually.
|
||||
let capturedOnEvent:
|
||||
| ((event: OrchestratorStreamEvent) => void)
|
||||
| null = null;
|
||||
vi.mock('./useOrchestratorStream', () => ({
|
||||
useOrchestratorStream: (opts: UseOrchestratorStreamOptions) => {
|
||||
capturedOnEvent = opts.onEvent;
|
||||
},
|
||||
}));
|
||||
|
||||
import api from '../utils/api';
|
||||
const apiGet = api.get as ReturnType<typeof vi.fn>;
|
||||
|
||||
const renderPage = () =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/orchestrator']}>
|
||||
<Orchestrator />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
describe('Orchestrator', () => {
|
||||
beforeEach(() => {
|
||||
capturedOnEvent = null;
|
||||
apiGet.mockReset();
|
||||
});
|
||||
|
||||
it('renders the empty state when the API returns no events', async () => {
|
||||
apiGet.mockResolvedValueOnce({ data: { data: [], total: 0 } });
|
||||
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByText(/NO ORCHESTRATOR ACTIVITY YET/i)).toBeInTheDocument();
|
||||
// The kind=all path advertises the orchestrator command, not the emailgen one.
|
||||
expect(screen.getByText(/decnet orchestrate/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches the kind filter and refetches scoped to that kind', async () => {
|
||||
apiGet.mockResolvedValue({ data: { data: [], total: 0 } });
|
||||
|
||||
renderPage();
|
||||
await waitFor(() => expect(apiGet).toHaveBeenCalledTimes(1));
|
||||
expect(apiGet.mock.calls[0][0]).toMatch(/^\/orchestrator\/events\?limit=50&offset=0$/);
|
||||
|
||||
await userEvent.click(screen.getByRole('tab', { name: /^email$/ }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(apiGet.mock.calls.some((c) => /kind=email/.test(c[0]))).toBe(true),
|
||||
);
|
||||
expect(screen.getByRole('tab', { name: /^email$/ })).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('prepends a row when the live stream pushes a traffic event', async () => {
|
||||
apiGet.mockResolvedValueOnce({ data: { data: [], total: 0 } });
|
||||
|
||||
renderPage();
|
||||
await waitFor(() => expect(capturedOnEvent).not.toBeNull());
|
||||
|
||||
act(() => {
|
||||
capturedOnEvent!({
|
||||
name: 'traffic',
|
||||
ts: new Date().toISOString(),
|
||||
payload: {
|
||||
kind: 'traffic',
|
||||
protocol: 'http',
|
||||
action: 'GET /admin',
|
||||
src_decky_uuid: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
||||
dst_decky_uuid: 'ffffffff-1111-2222-3333-444444444444',
|
||||
success: true,
|
||||
payload: '{}',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(await screen.findByText('GET /admin')).toBeInTheDocument();
|
||||
// 1 event shown after a single push.
|
||||
expect(screen.getByText(/1 EVENTS SHOWN/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
7
decnet_web/src/test/setup.ts
Normal file
7
decnet_web/src/test/setup.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { afterEach } from 'vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "esnext",
|
||||
"types": ["vite/client"],
|
||||
"types": ["vite/client", "vitest/globals", "@testing-library/jest-dom"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
css: false,
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'html'],
|
||||
include: ['src/**/*.{ts,tsx}'],
|
||||
exclude: ['src/**/*.d.ts', 'src/test/**', 'src/main.tsx'],
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
||||
@@ -512,15 +512,21 @@ have the badge call it on the same cadence the page already polls.
|
||||
Trigger: first time the count visibly diverges from a hand-checked
|
||||
DB query, or fleet size ≥ 10 active deckies.
|
||||
|
||||
### DEBT-043 — No frontend test framework configured
|
||||
**Files:** `decnet_web/package.json`
|
||||
The repo has no vitest/jest/RTL setup. Frontend changes (Orchestrator
|
||||
page, useOrchestratorStream hook, identity/campaign pages) ship with
|
||||
backend-only coverage. Component-level regressions land in production
|
||||
unless caught by manual smoke testing.
|
||||
**Remediation:** add vitest + @testing-library/react, write the
|
||||
listed-but-skipped tests for `Orchestrator.tsx` (renders empty state,
|
||||
filter toggling, mocked-EventSource prepend) as the seed suite.
|
||||
### ~~DEBT-043 — No frontend test framework configured~~ ✅ RESOLVED 2026-05-03
|
||||
**Files:** `decnet_web/package.json`, `decnet_web/vite.config.ts`,
|
||||
`decnet_web/src/test/setup.ts`, `decnet_web/src/components/Orchestrator.test.tsx`.
|
||||
vitest 4 + jsdom + @testing-library/{react,jest-dom,user-event} +
|
||||
@vitest/coverage-v8 wired through `vite.config.ts` (using
|
||||
`defineConfig` from `vitest/config` so the `test` block type-checks).
|
||||
`src/test/setup.ts` registers jest-dom matchers and runs RTL
|
||||
`cleanup` after each test. `tsconfig.app.json` picks up
|
||||
`vitest/globals` + `@testing-library/jest-dom` types. New scripts:
|
||||
`npm test` (watch), `npm run test:run` (one-shot), `npm run coverage`.
|
||||
Seed suite (`Orchestrator.test.tsx`) exercises the three regressions
|
||||
called out in the original entry: empty-state render, kind-filter
|
||||
toggling triggers a scoped refetch, and a mocked stream callback
|
||||
prepends a row to the table. Future component tests land alongside
|
||||
`*.tsx` as `*.test.tsx`.
|
||||
|
||||
### ~~DEBT-044 — `attacker.email.received` producer not wired~~ ✅ RESOLVED
|
||||
**Files:** `decnet/web/ingester.py`, `decnet/templates/smtp/server.py`
|
||||
@@ -715,7 +721,7 @@ user who needs it.
|
||||
| ~~DEBT-040~~ | ✅ | Honeypot / RDP+SMB cred framers | resolved |
|
||||
| ~~DEBT-041~~ | ✅ | API / UI / Threat-intel keying | resolved |
|
||||
| DEBT-042 | 🟢 Low | UI / Orchestrator failure-count window | open |
|
||||
| DEBT-043 | 🟡 Medium | Frontend test framework missing | open |
|
||||
| ~~DEBT-043~~ | ✅ | Frontend test framework missing | resolved 2026-05-03 |
|
||||
| ~~DEBT-044~~ | ✅ | TTP / Email producer wiring | resolved 2026-05-02 |
|
||||
| DEBT-045 | 🟡 Medium | TTP / EmailLifter heavyweight extraction | partial paid 2026-05-02 |
|
||||
| DEBT-046 | 🟡 Medium | TTP / EmailLifter mal-hash feed integration | open |
|
||||
@@ -723,5 +729,5 @@ user who needs it.
|
||||
| DEBT-048 | 🟡 Medium | TTP / Intel provider mapping review (recurring) | open / recurring |
|
||||
| DEBT-049 | 🟡 Medium | TTP / Sigma adapter (post-v1) | open |
|
||||
|
||||
**Remaining open:** DEBT-011 (Alembic), DEBT-027 (Dynamic bait store), DEBT-028 (deploy endpoint tests), DEBT-033 (transcript shard rotation), DEBT-036 (session-profile ingester), DEBT-037 (webhook delivery hardening), DEBT-038 (SSH PAM cred-capture limitations — document-only), DEBT-042 (orchestrator failure-count window), DEBT-043 (frontend test framework), DEBT-045 (EmailLifter heavyweight — partial paid; carved-out follow-ups remain), DEBT-046 (mal-hash feed), DEBT-048 (TTP intel provider mapping review — recurring quarterly), DEBT-049 (TTP Sigma adapter — post-v1).
|
||||
**Remaining open:** DEBT-011 (Alembic), DEBT-027 (Dynamic bait store), DEBT-028 (deploy endpoint tests), DEBT-033 (transcript shard rotation), DEBT-036 (session-profile ingester), DEBT-037 (webhook delivery hardening), DEBT-038 (SSH PAM cred-capture limitations — document-only), DEBT-042 (orchestrator failure-count window), DEBT-045 (EmailLifter heavyweight — partial paid; carved-out follow-ups remain), DEBT-046 (mal-hash feed), DEBT-048 (TTP intel provider mapping review — recurring quarterly), DEBT-049 (TTP Sigma adapter — post-v1).
|
||||
**Estimated remaining effort:** ~21 hours plus the new EmailLifter / TTP follow-ups. DEBT-030 Phase B (optimistic staged-buffer editor) is a follow-up, not debt.
|
||||
|
||||
Reference in New Issue
Block a user