test: Path Forge cancel and batch race UI coverage
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
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user