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] Progress bar updates in real-time showing current test name and percentage
- [x] Countdown timer counts down from 30 seconds - [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.) - [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 - [ ] Empty input returns 400, oversized >1MB returns 413
- [ ] Linting passes on both sides - [ ] Linting passes on both sides
- [ ] Run `/speckit.analyze` to verify consistency - [ ] 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 { afterEach, describe, expect, it, vi } from "vitest";
import Home from "../app/page"; import Home from "../app/page";
import type { AnalysisProgress, AnalysisReport } from "../types/analysis";
const { submitSpy, cancelSpy } = vi.hoisted(() => ({ const { submitSpy, cancelSpy, useAnalysisState } = vi.hoisted(() => {
submitSpy: vi.fn().mockResolvedValue(undefined), const submitSpy = vi.fn().mockResolvedValue(undefined);
cancelSpy: vi.fn(), const cancelSpy = vi.fn();
}));
return {
submitSpy,
cancelSpy,
useAnalysisState: {
status: "idle",
progress: null,
result: null,
error: null,
submit: submitSpy,
cancel: cancelSpy,
},
};
});
vi.mock("../hooks/useAnalysis", () => ({ vi.mock("../hooks/useAnalysis", () => ({
__esModule: true, __esModule: true,
default: () => ({ default: () => useAnalysisState,
status: "idle",
progress: null,
result: null,
error: null,
submit: submitSpy,
cancel: cancelSpy,
}),
})); }));
type RenderResult = { type RenderResult = {
@@ -66,6 +73,21 @@ const getAnalyseButton = (container: HTMLElement): HTMLButtonElement => {
return button as 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(() => { afterEach(() => {
while (cleanups.length > 0) { while (cleanups.length > 0) {
const cleanup = cleanups.pop(); const cleanup = cleanups.pop();
@@ -75,6 +97,7 @@ afterEach(() => {
} }
submitSpy.mockClear(); submitSpy.mockClear();
cancelSpy.mockClear(); cancelSpy.mockClear();
resetUseAnalysisState();
}); });
describe("Home page", () => { describe("Home page", () => {
@@ -98,4 +121,35 @@ describe("Home page", () => {
config: { testIds: [], resolve: false, decodeAll: false }, 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 isOversized = headerInput.length > MAX_HEADER_INPUT_BYTES;
const canAnalyse = hasHeaderInput && !isOversized; const canAnalyse = hasHeaderInput && !isOversized;
const isLoading = status === "submitting" || status === "analysing"; const isLoading = status === "submitting" || status === "analysing";
const showProgress = status === "analysing"; const showProgress = status === "analysing" || status === "timeout";
const incompleteTests = result?.metadata.incompleteTests ?? [];
const handleAnalyse = useCallback(() => { const handleAnalyse = useCallback(() => {
if (!canAnalyse) { if (!canAnalyse) {
@@ -86,6 +87,7 @@ export default function Home() {
status={status} status={status}
progress={progress} progress={progress}
timeoutSeconds={30} timeoutSeconds={30}
incompleteTests={incompleteTests}
/> />
) : null} ) : null}
</div> </div>