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:
2026-05-03 05:20:01 -04:00
parent 6c6f97e840
commit 866a76eccf
7 changed files with 1426 additions and 16 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View 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();
});
});

View File

@@ -0,0 +1,7 @@
import '@testing-library/jest-dom/vitest';
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
afterEach(() => {
cleanup();
});

View File

@@ -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 */

View File

@@ -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': {

View File

@@ -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.