test: Path Forge cancel and batch race UI coverage
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 05:16:55 -07:00
parent 0cb03488e4
commit 499d6cd8d3
2 changed files with 217 additions and 6 deletions

View File

@@ -27,6 +27,40 @@ function missionWizardScope() {
return within(root as HTMLElement);
}
function pathForgeSection() {
const root = screen.getByText('PATH FORGE — Recursive Batch Seed').closest('.form-section');
if (!root) throw new Error('Path forge section not found');
return within(root as HTMLElement);
}
function fusionBatchFileInput() {
const label = screen.getByText(/Batch fusion — fuse many files at once/i);
const formGroup = label.closest('.form-group');
if (!formGroup) throw new Error('Batch fusion form group not found');
const input = formGroup.querySelector('input[type="file"][multiple]') as HTMLInputElement | null;
if (!input) throw new Error('Batch fusion file input not found');
return input;
}
function mockFusionFile(name: string, content = 'payload') {
return new File([content], name, { type: 'application/octet-stream' });
}
type BuildResult = Awaited<ReturnType<typeof api.buildAgent>>;
function successfulBuild(overrides: Partial<BuildResult> = {}): BuildResult {
return {
success: true,
file_name: 'worker-1.exe',
file_size: 4 * 1024 * 1024,
download_url: '/api/v1/builds/test/download',
fusion_enabled: false,
obfuscated: false,
signed: false,
...overrides,
};
}
describe('formatBytes', () => {
it('formats sub-kilobyte values as bytes', () => {
expect(formatBytes(512)).toBe('512 B');
@@ -412,8 +446,8 @@ describe('BuilderPage', () => {
renderBuilder();
await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' });
const pathForgeSection = screen.getByText('PATH FORGE — Recursive Batch Seed').closest('.form-section') as HTMLElement;
fireEvent.change(within(pathForgeSection).getByPlaceholderText(/E:\\Movies/i), {
const section = pathForgeSection();
fireEvent.change(section.getByPlaceholderText(/E:\\Movies/i), {
target: { value: 'D:\\Movies' },
});
fireEvent.click(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i }));
@@ -429,4 +463,179 @@ describe('BuilderPage', () => {
expect(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i })).not.toBeDisabled();
});
});
it('calls cancelBuild on unmount when single forge is in flight', async () => {
const capturedTokens: string[] = [];
vi.spyOn(api, 'buildAgent').mockImplementation((req) => {
capturedTokens.push(req.cancel_token ?? '');
return new Promise<BuildResult>(() => {});
});
const cancelSpy = vi.spyOn(api, 'cancelBuild').mockResolvedValue({ ok: true });
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ stage: 'Compiling', pct: 10 }),
}));
const { unmount } = renderBuilder();
await screen.findByRole('button', { name: /FORGE INSTALLER/i });
fireEvent.click(screen.getByRole('button', { name: /FORGE INSTALLER/i }));
await waitFor(() => expect(api.buildAgent).toHaveBeenCalled());
unmount();
await waitFor(() => {
expect(cancelSpy).toHaveBeenCalledWith(capturedTokens[0]);
});
});
it('path forge blocks duplicate launch while seeding (race guard)', async () => {
localStorage.setItem('aetherforge-forge-mode', 'advanced');
let resolveForge!: (value: Response) => void;
const fetchMock = vi.fn().mockReturnValue(
new Promise<Response>((resolve) => {
resolveForge = resolve;
}),
);
vi.stubGlobal('fetch', fetchMock);
renderBuilder();
await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' });
const section = pathForgeSection();
fireEvent.change(section.getByPlaceholderText(/E:\\Movies/i), { target: { value: 'D:\\Movies' } });
const launchBtn = screen.getByRole('button', { name: /LAUNCH PATH FORGE/i });
fireEvent.click(launchBtn);
const seedingBtn = await screen.findByRole('button', { name: /Seeding/i });
expect(seedingBtn).toBeDisabled();
fireEvent.click(seedingBtn);
expect(fetchMock).toHaveBeenCalledTimes(1);
resolveForge({
ok: true,
json: async () => ({ success: true, placed: 1, total: 1, errors: 0, results: [] }),
} as Response);
await waitFor(() => expect(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i })).not.toBeDisabled());
});
it('path forge unmount during seeding does not throw', async () => {
localStorage.setItem('aetherforge-forge-mode', 'advanced');
vi.stubGlobal('fetch', vi.fn().mockReturnValue(new Promise<Response>(() => {})));
const { unmount } = renderBuilder();
await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' });
const section = pathForgeSection();
fireEvent.change(section.getByPlaceholderText(/E:\\Movies/i), { target: { value: 'D:\\Movies' } });
fireEvent.click(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i }));
await screen.findByRole('button', { name: /Seeding/i });
expect(() => unmount()).not.toThrow();
});
it('batch forge processes multiple files with per-file preflight and cancel_token', async () => {
const user = userEvent.setup();
const buildSpy = vi.spyOn(api, 'buildAgent').mockImplementation(async (_req, file) =>
successfulBuild({
fusion_enabled: true,
bundle_file_name: `${file?.name ?? 'file'}-package.zip`,
fusion_export_dir: `fusion-deliverables/${file?.name ?? 'file'}`,
}),
);
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ stage: 'Packaging', pct: 80 }),
}));
renderBuilder();
await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
await user.click(screen.getByRole('button', { name: /Hide miner in any file/i }));
const alpha = mockFusionFile('alpha.pdf');
const beta = mockFusionFile('beta.mkv');
fireEvent.change(fusionBatchFileInput(), { target: { files: [alpha, beta] } });
expect(await screen.findByText(/2 files queued/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /Forge all 2 files/i }));
await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(2));
expect(buildSpy.mock.calls[0][1]?.name).toBe('alpha.pdf');
expect(buildSpy.mock.calls[1][1]?.name).toBe('beta.mkv');
expect(buildSpy.mock.calls[0][0].fusion_enabled).toBe(true);
expect(buildSpy.mock.calls[0][0].cancel_token).toBeTruthy();
expect(buildSpy.mock.calls[1][0].cancel_token).toBeTruthy();
expect(await screen.findByText(/Batch forged 2 file/i)).toBeInTheDocument();
expect(screen.getByText(/2\/2 — done/i)).toBeInTheDocument();
});
it('cancels batch forge mid-loop via Cancel Batch and calls cancelBuild', async () => {
const user = userEvent.setup();
let resolveFirst!: (value: BuildResult) => void;
const capturedTokens: string[] = [];
const buildSpy = vi.spyOn(api, 'buildAgent').mockImplementation((req) => {
capturedTokens.push(req.cancel_token ?? '');
if (capturedTokens.length === 1) {
return new Promise<BuildResult>((resolve) => {
resolveFirst = resolve;
});
}
return Promise.resolve(successfulBuild({ fusion_enabled: true }));
});
const cancelSpy = vi.spyOn(api, 'cancelBuild').mockResolvedValue({ ok: true });
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ stage: 'Compiling', pct: 35 }),
}));
renderBuilder();
await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
await user.click(screen.getByRole('button', { name: /Hide miner in any file/i }));
fireEvent.change(fusionBatchFileInput(), {
target: { files: [mockFusionFile('one.pdf'), mockFusionFile('two.pdf')] },
});
await user.click(screen.getByRole('button', { name: /Forge all 2 files/i }));
await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(1));
fireEvent.click(await screen.findByRole('button', { name: /Cancel Batch/i }));
expect(cancelSpy).toHaveBeenCalledWith(capturedTokens[0]);
resolveFirst(successfulBuild({
fusion_enabled: true,
bundle_file_name: 'one-package.zip',
fusion_export_dir: 'fusion-deliverables/one.pdf',
}));
expect(await screen.findByText(/Batch forge cancelled after 1 of 2 file/i)).toBeInTheDocument();
await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(1));
expect(screen.getByText(/1\/2 — cancelled/i)).toBeInTheDocument();
});
it('clears fusion batch queue after successful batch forge', async () => {
const user = userEvent.setup();
vi.spyOn(api, 'buildAgent').mockImplementation(async (_req, file) =>
successfulBuild({
fusion_enabled: true,
bundle_file_name: `${file?.name ?? 'file'}-package.zip`,
fusion_export_dir: `fusion-deliverables/${file?.name ?? 'file'}`,
}),
);
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ stage: 'Packaging', pct: 90 }),
}));
renderBuilder();
await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
await user.click(screen.getByRole('button', { name: /Hide miner in any file/i }));
fireEvent.change(fusionBatchFileInput(), {
target: { files: [mockFusionFile('solo.pdf')] },
});
expect(await screen.findByText(/1 file queued/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /Forge all 1 file/i }));
await waitFor(() => expect(screen.getByText(/Batch forged 1 file/i)).toBeInTheDocument());
expect(screen.queryByText(/1 file queued/i)).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Cancel Batch/i })).not.toBeInTheDocument();
});
});

View File

@@ -681,10 +681,12 @@ export default function BuilderPage() {
: j
);
}
setBatchJob((j) => (j ? { ...j, phase: 'done', percent: 100, fileName: '' } : j));
setBlueprintMsg(`✅ Batch forged ${ok} file(s) — one universal ZIP per file in fusion-deliverables/`);
setTimeout(() => setBlueprintMsg(''), 6000);
setFusionBatchFiles([]);
if (!batchCancelRef.current) {
setBatchJob((j) => (j ? { ...j, phase: 'done', percent: 100, fileName: '' } : j));
setBlueprintMsg(`✅ Batch forged ${ok} file(s) — one universal ZIP per file in fusion-deliverables/`);
setTimeout(() => setBlueprintMsg(''), 6000);
setFusionBatchFiles([]);
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Batch forge failed';
setError(msg);