MAESTRO: show timeout notification

This commit is contained in:
Mariusz Banach
2026-02-18 02:32:35 +01:00
parent bb6b84b470
commit 355e23e063
3 changed files with 70 additions and 14 deletions

View File

@@ -45,7 +45,7 @@ Frontend uses `fetch` with `ReadableStream` reader (not native `EventSource`, wh
- [x] Progress bar updates in real-time showing current test name and percentage
- [x] Countdown timer counts down from 30 seconds
- [x] Partial failures show inline error indicators per FR-25 (Added AnalysisResults rendering with inline error badges.)
- [ ] Timeout at 30s displays partial results with notification listing incomplete tests
- [x] Timeout at 30s displays partial results with notification listing incomplete tests
- [ ] Empty input returns 400, oversized >1MB returns 413
- [ ] Linting passes on both sides
- [ ] Run `/speckit.analyze` to verify consistency

View File

@@ -4,22 +4,29 @@ import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import Home from "../app/page";
import type { AnalysisProgress, AnalysisReport } from "../types/analysis";
const { submitSpy, cancelSpy } = vi.hoisted(() => ({
submitSpy: vi.fn().mockResolvedValue(undefined),
cancelSpy: vi.fn(),
}));
const { submitSpy, cancelSpy, useAnalysisState } = vi.hoisted(() => {
const submitSpy = vi.fn().mockResolvedValue(undefined);
const cancelSpy = vi.fn();
return {
submitSpy,
cancelSpy,
useAnalysisState: {
status: "idle",
progress: null,
result: null,
error: null,
submit: submitSpy,
cancel: cancelSpy,
},
};
});
vi.mock("../hooks/useAnalysis", () => ({
__esModule: true,
default: () => ({
status: "idle",
progress: null,
result: null,
error: null,
submit: submitSpy,
cancel: cancelSpy,
}),
default: () => useAnalysisState,
}));
type RenderResult = {
@@ -66,6 +73,21 @@ const getAnalyseButton = (container: HTMLElement): HTMLButtonElement => {
return button as HTMLButtonElement;
};
const baseProgress: AnalysisProgress = {
currentIndex: 2,
totalTests: 4,
currentTest: "SpamAssassin Rule Hits",
elapsedMs: 29000,
percentage: 78,
};
const resetUseAnalysisState = (): void => {
useAnalysisState.status = "idle";
useAnalysisState.progress = null;
useAnalysisState.result = null;
useAnalysisState.error = null;
};
afterEach(() => {
while (cleanups.length > 0) {
const cleanup = cleanups.pop();
@@ -75,6 +97,7 @@ afterEach(() => {
}
submitSpy.mockClear();
cancelSpy.mockClear();
resetUseAnalysisState();
});
describe("Home page", () => {
@@ -98,4 +121,35 @@ describe("Home page", () => {
config: { testIds: [], resolve: false, decodeAll: false },
});
});
it("shows timeout notification with incomplete tests and partial results", () => {
const timeoutReport: AnalysisReport = {
results: [],
hopChain: [],
securityAppliances: [],
metadata: {
totalTests: 4,
passedTests: 1,
failedTests: 0,
skippedTests: 0,
elapsedMs: 30000,
timedOut: true,
incompleteTests: ["Mimecast Fingerprint", "Proofpoint TAP"],
},
};
useAnalysisState.status = "timeout";
useAnalysisState.progress = baseProgress;
useAnalysisState.result = timeoutReport;
const { container } = render(<Home />);
const alert = container.querySelector('[role="alert"]');
expect(alert).not.toBeNull();
const timeoutTests = container.querySelector('[data-testid="timeout-tests"]');
expect(timeoutTests?.textContent ?? "").toMatch(/Mimecast Fingerprint/);
expect(timeoutTests?.textContent ?? "").toMatch(/Proofpoint TAP/);
const results = container.querySelector('[data-testid="analysis-results"]');
expect(results).not.toBeNull();
});
});

View File

@@ -24,7 +24,8 @@ export default function Home() {
const isOversized = headerInput.length > MAX_HEADER_INPUT_BYTES;
const canAnalyse = hasHeaderInput && !isOversized;
const isLoading = status === "submitting" || status === "analysing";
const showProgress = status === "analysing";
const showProgress = status === "analysing" || status === "timeout";
const incompleteTests = result?.metadata.incompleteTests ?? [];
const handleAnalyse = useCallback(() => {
if (!canAnalyse) {
@@ -86,6 +87,7 @@ export default function Home() {
status={status}
progress={progress}
timeoutSeconds={30}
incompleteTests={incompleteTests}
/>
) : null}
</div>