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
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'npm'

- name: Install dependencies
Expand All @@ -27,12 +27,21 @@ jobs:
- name: Lint
run: npm run lint

- name: Test
run: npm run test:coverage

- name: Build
run: npm run build

- name: Type check
run: npm run typecheck || npx tsc --noEmit

- name: Upload coverage
uses: codecov/codecov-action@v4
if: github.event_name == 'push'
with:
fail_ci_if_error: false

# Optional: Publish to npm on tagged releases
publish:
name: Publish to npm
Expand All @@ -49,7 +58,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'

Expand Down
12 changes: 12 additions & 0 deletions __tests__/app/api/metrics/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Simple tests for the metrics route
// Note: Full API tests require Node.js runtime, skip in jsdom

describe('Metrics API - Structure', () => {
it('should have the GET handler exported', async () => {
// We can't test the actual handler in jsdom due to NextResponse
// But we can verify the module structure
const route = await import('@/app/api/metrics/route');
expect(route).toHaveProperty('GET');
expect(typeof route.GET).toBe('function');
});
});
90 changes: 90 additions & 0 deletions __tests__/components/CpuTab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { render } from '@testing-library/react';
import CpuTab from '@/lib/components/CpuTab';
import type { CpuMetrics } from '@/types/metrics';
import { screen } from '@testing-library/dom';

const mockCpuData: CpuMetrics = {
name: 'AMD Ryzen 9 7950X',
usage: 45,
usageUser: 30,
usageSystem: 15,
physicalCores: 16,
logicalCores: 32,
temperature: 65,
speed: 4500,
currentSpeedMHz: 4500,
maxSpeedMHz: 5700,
minSpeedMHz: 3000,
loadAvg: [2.5, 2.3, 2.1],
coreLoads: [45, 50, 40, 42],
coreSpeeds: [4.5, 4.6, 4.4, 4.5],
flags: 'avx512f avx512dq avx512cd avx512bw avx512vl',
virtualization: true,
governor: 'performance',
};

describe('CpuTab', () => {
it('renders loading state when data is null', () => {
render(<CpuTab data={null} />);
expect(screen.getByText('Loading CPU metrics...')).toBeInTheDocument();
});

it('renders CPU name and basic info', () => {
render(<CpuTab data={mockCpuData} />);
expect(screen.getByText('AMD Ryzen 9 7950X')).toBeInTheDocument();
// Check for cores/threads text in the document
const { container } = render(<CpuTab data={mockCpuData} />);
expect(container.textContent).toContain('16 cores');
expect(container.textContent).toContain('32 threads');
});

it('displays CPU usage percentage', () => {
const { container } = render(<CpuTab data={mockCpuData} />);
expect(container.textContent).toContain('45%');
});

it('displays temperature when available', () => {
const { container } = render(<CpuTab data={mockCpuData} />);
expect(container.textContent).toContain('65°C');
});

it('displays load averages', () => {
const { container } = render(<CpuTab data={mockCpuData} />);
expect(container.textContent).toContain('2.50');
expect(container.textContent).toContain('2.30');
expect(container.textContent).toContain('2.10');
});

it('renders per-core usage bars', () => {
render(<CpuTab data={mockCpuData} />);
// Check for core labels
expect(screen.getByText('C0')).toBeInTheDocument();
expect(screen.getByText('C1')).toBeInTheDocument();
expect(screen.getByText('C2')).toBeInTheDocument();
expect(screen.getByText('C3')).toBeInTheDocument();
});

it('handles missing temperature gracefully', () => {
const noTempData: CpuMetrics = { ...mockCpuData, temperature: null };
render(<CpuTab data={noTempData} />);
// Should still render without temperature
expect(screen.getByText('AMD Ryzen 9 7950X')).toBeInTheDocument();
});

it('shows alert styling for high CPU usage', () => {
const highUsageData: CpuMetrics = { ...mockCpuData, usage: 85 };
const { container } = render(<CpuTab data={highUsageData} />);
expect(container.textContent).toContain('85%');
});

it('handles Intel CPU naming', () => {
const intelData: CpuMetrics = {
...mockCpuData,
name: 'Intel Core i9-14900K',
physicalCores: 8,
logicalCores: 16,
};
render(<CpuTab data={intelData} />);
expect(screen.getByText('Intel Core i9-14900K')).toBeInTheDocument();
});
});
102 changes: 102 additions & 0 deletions __tests__/components/Dashboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { SystemMetrics } from '@/types/metrics';

// Simple smoke test - Dashboard is complex with providers,
// component-level tests cover the functionality
describe('Dashboard', () => {
it('has required mock data structure', () => {
const mockMetrics: SystemMetrics = {
timestamp: Date.now(),
cpu: {
name: 'AMD Ryzen 9 7950X',
usage: 45,
usageUser: 30,
usageSystem: 15,
physicalCores: 16,
logicalCores: 32,
temperature: 65,
speed: 4500,
currentSpeedMHz: 4500,
maxSpeedMHz: 5700,
minSpeedMHz: 3000,
loadAvg: [2.5, 2.3, 2.1],
coreLoads: [45, 50, 40, 42],
coreSpeeds: [4.5, 4.6, 4.4, 4.5],
flags: 'avx512f',
virtualization: true,
governor: 'performance',
topProcesses: [
{ pid: 1234, name: 'chrome', cpu: 12.5, mem: 8.2, user: 'user' },
],
},
memory: {
total: 32,
used: 16,
free: 16,
usage: 50,
swapTotal: 8,
swapUsed: 2,
swapFree: 6,
},
gpu: [
{
index: 0,
name: 'gfx1150',
marketingName: 'AMD Radeon 890M',
vendor: 'AMD',
usage: 75,
memory: { total: 32, used: 16 },
temperature: 65,
driverVersion: '6.3.6',
gfxVersion: 'gfx1150',
deviceId: '0x150e',
computeUnits: 16,
maxClockMHz: 2800,
currentClockMHz: 1500,
},
],
network: {
interfaces: [
{
name: 'eth0',
ip4: '192.168.1.100',
ip6: '',
speed: 1000,
rxSec: 1024,
txSec: 512,
rxBytes: 1073741824,
txBytes: 536870912,
},
],
total: {
rxSec: 1024,
txSec: 512,
},
},
disk: {
disks: [
{ name: '/', total: 500, used: 250, free: 250, usage: 50 },
],
total: {
total: 500,
used: 250,
free: 250,
},
},
os: {
platform: 'linux',
distro: 'Ubuntu',
release: '24.04',
hostname: 'workstation',
arch: 'x64',
},
rocmDetected: true,
rocmRuntimeVersion: '7.2.0',
};

// Verify mock data structure
expect(mockMetrics).toBeDefined();
expect(mockMetrics.cpu.name).toBe('AMD Ryzen 9 7950X');
expect(mockMetrics.gpu).toHaveLength(1);
expect(mockMetrics.gpu[0].marketingName).toBe('AMD Radeon 890M');
});
});
100 changes: 100 additions & 0 deletions __tests__/components/GpuTab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { render } from '@testing-library/react';
import GpuTab from '@/lib/components/GpuTab';
import type { GpuMetrics } from '@/types/metrics';
import { screen } from '@testing-library/dom';

const mockGpu: GpuMetrics = {
index: 0,
name: 'gfx1150',
marketingName: 'AMD Radeon 890M',
vendor: 'AMD',
usage: 75,
memory: { total: 32, used: 16 },
temperature: 45,
power: 65.5,
driverVersion: '6.3.6',
gfxVersion: 'gfx1150',
deviceId: '0x150e',
computeUnits: 16,
maxClockMHz: 2800,
currentClockMHz: 1500,
};

const mockSecondaryGpu: GpuMetrics = {
index: 1,
name: 'gfx1100',
marketingName: 'AMD Radeon RX 7900 XTX',
vendor: 'AMD',
usage: 90,
memory: { total: 24, used: 20 },
temperature: 72,
driverVersion: '6.3.6',
gfxVersion: 'gfx1100',
deviceId: '0x744c',
computeUnits: 96,
maxClockMHz: 2500,
currentClockMHz: 2300,
};

describe('GpuTab', () => {
it('renders "No GPU detected" when empty', () => {
render(<GpuTab gpus={[]} />);
expect(screen.getByText('No GPU detected')).toBeInTheDocument();
});

it('renders primary GPU marketing name', () => {
render(<GpuTab gpus={[mockGpu]} />);
expect(screen.getByText('AMD Radeon 890M')).toBeInTheDocument();
});

it('displays GPU usage percentage', () => {
const { container } = render(<GpuTab gpus={[mockGpu]} />);
expect(container.textContent).toContain('75%');
});

it('displays VRAM usage', () => {
const { container } = render(<GpuTab gpus={[mockGpu]} />);
expect(container.textContent).toContain('VRAM');
});

it('displays temperature when available', () => {
const { container } = render(<GpuTab gpus={[mockGpu]} />);
expect(container.textContent).toContain('45°C');
});

it('displays power consumption when available', () => {
const { container } = render(<GpuTab gpus={[mockGpu]} />);
expect(container.textContent).toContain('65.5W');
});

it('renders additional GPUs section when multiple GPUs', () => {
render(<GpuTab gpus={[mockGpu, mockSecondaryGpu]} />);
expect(screen.getByText('Additional GPUs')).toBeInTheDocument();
expect(screen.getByText('AMD Radeon RX 7900 XTX')).toBeInTheDocument();
});

it('displays GPU specs', () => {
const { container } = render(<GpuTab gpus={[mockGpu]} />);
expect(container.textContent).toContain('16 CUs');
expect(container.textContent).toContain('gfx1150');
});

it('handles missing optional properties', () => {
const minimalGpu: GpuMetrics = {
index: 0,
name: 'Unknown GPU',
marketingName: 'Unknown GPU',
vendor: 'AMD',
usage: 0,
memory: { total: 0, used: 0 },
driverVersion: 'unknown',
gfxVersion: 'N/A',
deviceId: 'N/A',
computeUnits: 0,
maxClockMHz: 0,
currentClockMHz: 0,
};
render(<GpuTab gpus={[minimalGpu]} />);
expect(screen.getByText('Unknown GPU')).toBeInTheDocument();
});
});
Loading
Loading