mirror of
https://github.com/mgeeky/decode-spam-headers.git
synced 2026-02-22 05:23:31 +01:00
MAESTRO: show timeout notification
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user