Final sweep: Crucible fixes, Path Tracer polish, forge progress, tests green.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Align dashboard subtitle default and UpsertAgent tests with fleet label behavior; WebSocket coalesce and PathForge hardening; Crucible expanded ops and visual DV fixes; Vitest 610/610 and full test-suite pass; trim PROBLEMS.md to open items only.
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { mockAgent } from '../test/fixtures';
|
||||
import {
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import { api } from '../api/client';
|
||||
import CruciblePage, {
|
||||
agentColor,
|
||||
patchLabel,
|
||||
pendingBadge,
|
||||
@@ -11,6 +18,201 @@ import {
|
||||
thermalBadge,
|
||||
} from './CruciblePage';
|
||||
|
||||
// ── Mocks ─────────────────────────────────────────────────────────────────
|
||||
|
||||
vi.mock('../hooks/useWebSocket', () => ({ useWebSocket: vi.fn() }));
|
||||
|
||||
vi.mock('../context/MatrixRainContext', () => ({
|
||||
useMatrixRain: () => ({ setCrucibleFocus: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useFleetGroups', () => ({
|
||||
useFleetGroups: () => ({ groups: [], addGroup: vi.fn(), removeGroup: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('../api/client', () => ({
|
||||
api: {
|
||||
sendAgentCommand: vi.fn().mockResolvedValue({ success: true }),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../components/Fleet/CrucibleExpandedOps', () => ({
|
||||
default: () => <div data-testid="expanded-ops" />,
|
||||
}));
|
||||
|
||||
vi.mock('../components/Fleet/FleetHeatMiniMap', () => ({
|
||||
default: () => <div data-testid="heat-map" />,
|
||||
}));
|
||||
|
||||
vi.mock('../components/Fleet/LatencyBadge', () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../components/Fleet/CreateGroupModal', () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../components/Fleet/FleetGroupsStrip', () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../components/Presence/AlsoHere', () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../components/Fleet/FullSysCheckPanel', () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../components/HelpTip', () => ({
|
||||
HelpTip: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../components/NeonCard/NeonCard', () => ({
|
||||
default: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
||||
<div className={className}>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const useWebSocketMock = vi.mocked(useWebSocket);
|
||||
|
||||
function makeWsValue(overrides: Partial<ReturnType<typeof useWebSocket>>) {
|
||||
return {
|
||||
isConnected: true,
|
||||
agents: [],
|
||||
recentShares: [],
|
||||
fleetAlerts: [],
|
||||
poolStatus: [],
|
||||
aiActivity: [],
|
||||
agentLogs: {},
|
||||
commandResults: [],
|
||||
policyAcks: [],
|
||||
latestMessage: null,
|
||||
sendDashboardMessage: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function renderCrucible(wsValue: ReturnType<typeof makeWsValue>) {
|
||||
useWebSocketMock.mockReturnValue(wsValue as ReturnType<typeof useWebSocket>);
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<CruciblePage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Terminal rendering tests ──────────────────────────────────────────────
|
||||
|
||||
describe('CruciblePage terminal — command_result processing', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('displays exec result in terminal when agent is selected', async () => {
|
||||
const agent = mockAgent({ id: 'agent-aaa-001', name: 'TestNode', status: 'online' });
|
||||
const commandResults = [
|
||||
{ agent_id: 'agent-aaa-001', action: 'exec', success: true, message: 'hello world', _seq: 1 },
|
||||
];
|
||||
|
||||
// Render with the agent already "selected" by providing a pre-selected state.
|
||||
// The component reads commandResults from context and renders terminal lines.
|
||||
renderCrucible(makeWsValue({ agents: [agent], commandResults }));
|
||||
|
||||
// The terminal should display the result message from the command.
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('hello world')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays multiple exec result lines split on newline', async () => {
|
||||
const agent = mockAgent({ id: 'agent-bbb-002', name: 'MultiNode', status: 'online' });
|
||||
const commandResults = [
|
||||
{
|
||||
agent_id: 'agent-bbb-002',
|
||||
action: 'exec',
|
||||
success: true,
|
||||
message: 'line one\nline two\nline three',
|
||||
_seq: 1,
|
||||
},
|
||||
];
|
||||
|
||||
renderCrucible(makeWsValue({ agents: [agent], commandResults }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('line one')).toBeInTheDocument();
|
||||
expect(screen.getByText('line two')).toBeInTheDocument();
|
||||
expect(screen.getByText('line three')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('skips command_result entries with missing agent_id', async () => {
|
||||
const commandResults = [
|
||||
{ agent_id: undefined, action: 'exec', success: true, message: 'ghost output', _seq: 1 },
|
||||
];
|
||||
|
||||
renderCrucible(makeWsValue({ commandResults }));
|
||||
|
||||
// "ghost output" should not appear — entry has no agent_id so it's skipped.
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
expect(screen.queryByText('ghost output')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('skips already-seen entries when new commandResults arrive', async () => {
|
||||
const agent = mockAgent({ id: 'agent-ccc-003', name: 'SeqNode', status: 'online' });
|
||||
|
||||
// Initial render: seq=1
|
||||
const ws1 = makeWsValue({
|
||||
agents: [agent],
|
||||
commandResults: [{ agent_id: 'agent-ccc-003', action: 'exec', success: true, message: 'first', _seq: 1 }],
|
||||
});
|
||||
const { rerender } = renderCrucible(ws1);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('first')).toBeInTheDocument());
|
||||
|
||||
// Update: add seq=2, keep seq=1 — only 'second' should be added (not 'first' again)
|
||||
const ws2 = makeWsValue({
|
||||
agents: [agent],
|
||||
commandResults: [
|
||||
{ agent_id: 'agent-ccc-003', action: 'exec', success: true, message: 'first', _seq: 1 },
|
||||
{ agent_id: 'agent-ccc-003', action: 'exec', success: true, message: 'second', _seq: 2 },
|
||||
],
|
||||
});
|
||||
useWebSocketMock.mockReturnValue(ws2 as ReturnType<typeof useWebSocket>);
|
||||
rerender(
|
||||
<MemoryRouter>
|
||||
<CruciblePage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('second')).toBeInTheDocument());
|
||||
// 'first' should appear exactly once (not twice from a replay)
|
||||
expect(screen.getAllByText('first')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows fallback line when command result message is empty', async () => {
|
||||
const agent = mockAgent({ id: 'agent-empty-001', name: 'EmptyNode', status: 'online' });
|
||||
renderCrucible(
|
||||
makeWsValue({
|
||||
agents: [agent],
|
||||
commandResults: [
|
||||
{ agent_id: 'agent-empty-001', action: 'pause', success: true, message: '', _seq: 1 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('[pause] OK')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Helper function tests ─────────────────────────────────────────────────
|
||||
|
||||
describe('CruciblePage helpers', () => {
|
||||
it('agentColor cycles palette by agent order', () => {
|
||||
const ids = ['a', 'b', 'c'];
|
||||
|
||||
Reference in New Issue
Block a user