MAESTRO: fix linting issues

This commit is contained in:
Mariusz Banach
2026-02-18 02:38:48 +01:00
parent 00dd99f8fa
commit 5598b01eaf
5 changed files with 64 additions and 31 deletions

View File

@@ -47,5 +47,5 @@ Frontend uses `fetch` with `ReadableStream` reader (not native `EventSource`, wh
- [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.)
- [x] Timeout at 30s displays partial results with notification listing incomplete tests - [x] Timeout at 30s displays partial results with notification listing incomplete tests
- [x] Empty input returns 400, oversized >1MB returns 413 - [x] Empty input returns 400, oversized >1MB returns 413
- [ ] Linting passes on both sides - [x] Linting passes on both sides
- [ ] Run `/speckit.analyze` to verify consistency - [ ] Run `/speckit.analyze` to verify consistency

View File

@@ -4,7 +4,11 @@ from pydantic import BaseModel, ConfigDict, Field
from app.engine.models import ( from app.engine.models import (
AnalysisConfig, AnalysisConfig,
)
from app.engine.models import (
AnalysisRequest as EngineAnalysisRequest, AnalysisRequest as EngineAnalysisRequest,
)
from app.engine.models import (
AnalysisResult as EngineAnalysisResult, AnalysisResult as EngineAnalysisResult,
) )

View File

@@ -8,7 +8,13 @@ import pytest
from httpx import ASGITransport, AsyncClient from httpx import ASGITransport, AsyncClient
from app.engine.analyzer import HeaderAnalyzer from app.engine.analyzer import HeaderAnalyzer
from app.engine.models import AnalysisResult, ReportMetadata, Severity, TestResult, TestStatus from app.engine.models import (
AnalysisResult,
ReportMetadata,
Severity,
TestResult,
TestStatus,
)
from app.main import app from app.main import app
FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures" FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures"
@@ -44,7 +50,10 @@ def _parse_sse_events(raw: str) -> list[dict[str, Any]]:
return events return events
async def _collect_stream_events(client: AsyncClient, payload: dict[str, Any]) -> list[dict[str, Any]]: async def _collect_stream_events(
client: AsyncClient,
payload: dict[str, Any],
) -> list[dict[str, Any]]:
async with client.stream( async with client.stream(
"POST", "POST",
"/api/analyse", "/api/analyse",
@@ -99,7 +108,10 @@ async def test_analyse_streams_progress_and_result() -> None:
@pytest.mark.anyio @pytest.mark.anyio
async def test_analyse_rejects_empty_headers() -> None: async def test_analyse_rejects_empty_headers() -> None:
payload = {"headers": "", "config": {"testIds": [], "resolve": False, "decodeAll": False}} payload = {
"headers": "",
"config": {"testIds": [], "resolve": False, "decodeAll": False},
}
async with AsyncClient( async with AsyncClient(
transport=ASGITransport(app=app), transport=ASGITransport(app=app),
@@ -131,7 +143,9 @@ async def test_analyse_rejects_oversized_headers() -> None:
@pytest.mark.anyio @pytest.mark.anyio
async def test_analyse_stream_includes_partial_failures(monkeypatch: pytest.MonkeyPatch) -> None: async def test_analyse_stream_includes_partial_failures(
monkeypatch: pytest.MonkeyPatch,
) -> None:
raw_headers = (FIXTURES_DIR / "sample_headers.txt").read_text(encoding="utf-8") raw_headers = (FIXTURES_DIR / "sample_headers.txt").read_text(encoding="utf-8")
def fake_analyze( def fake_analyze(
@@ -191,17 +205,23 @@ async def test_analyse_stream_includes_partial_failures(monkeypatch: pytest.Monk
) as client: ) as client:
events = await _collect_stream_events(client, payload) events = await _collect_stream_events(client, payload)
result_payload = next(event["data"] for event in events if event["event"] == "result") result_payload = next(
event["data"] for event in events if event["event"] == "result"
)
statuses = [item["status"] for item in result_payload["results"]] statuses = [item["status"] for item in result_payload["results"]]
assert "error" in statuses assert "error" in statuses
error_entries = [item for item in result_payload["results"] if item["status"] == "error"] error_entries = [
item for item in result_payload["results"] if item["status"] == "error"
]
assert error_entries[0]["error"] assert error_entries[0]["error"]
assert result_payload["metadata"]["failedTests"] == 1 assert result_payload["metadata"]["failedTests"] == 1
assert result_payload["metadata"]["incompleteTests"] == ["Test B"] assert result_payload["metadata"]["incompleteTests"] == ["Test B"]
@pytest.mark.anyio @pytest.mark.anyio
async def test_analyse_times_out_with_partial_results(monkeypatch: pytest.MonkeyPatch) -> None: async def test_analyse_times_out_with_partial_results(
monkeypatch: pytest.MonkeyPatch,
) -> None:
raw_headers = (FIXTURES_DIR / "sample_headers.txt").read_text(encoding="utf-8") raw_headers = (FIXTURES_DIR / "sample_headers.txt").read_text(encoding="utf-8")
def fake_analyze( def fake_analyze(
@@ -227,6 +247,8 @@ async def test_analyse_times_out_with_partial_results(monkeypatch: pytest.Monkey
) as client: ) as client:
events = await _collect_stream_events(client, payload) events = await _collect_stream_events(client, payload)
result_payload = next(event["data"] for event in events if event["event"] == "result") result_payload = next(
event["data"] for event in events if event["event"] == "result"
)
assert result_payload["metadata"]["timedOut"] is True assert result_payload["metadata"]["timedOut"] is True
assert result_payload["metadata"]["incompleteTests"] assert result_payload["metadata"]["incompleteTests"]

View File

@@ -1,4 +1,6 @@
import coreWebVitals from "eslint-config-next/core-web-vitals"; import coreWebVitals from "eslint-config-next/core-web-vitals";
import typescript from "eslint-config-next/typescript"; import typescript from "eslint-config-next/typescript";
export default [...coreWebVitals, ...typescript]; const config = [...coreWebVitals, ...typescript];
export default config;

View File

@@ -63,24 +63,15 @@ export default function ProgressIndicator({
timeoutSeconds, timeoutSeconds,
incompleteTests = [], incompleteTests = [],
}: ProgressIndicatorProps) { }: ProgressIndicatorProps) {
const [nowMs, setNowMs] = useState(() => Date.now()); const [elapsedMs, setElapsedMs] = useState(() => progress?.elapsedMs ?? 0);
const progressRef = useRef<AnalysisProgress | null>(progress);
const statusRef = useRef<AnalysisStatus>(status);
const anchorRef = useRef<{ elapsedMs: number; timestamp: number } | null>(null); const anchorRef = useRef<{ elapsedMs: number; timestamp: number } | null>(null);
useEffect(() => { useEffect(() => {
if (status !== "analysing") { progressRef.current = progress;
return; statusRef.current = status;
}
const interval = window.setInterval(() => {
setNowMs(Date.now());
}, 1000);
return () => {
window.clearInterval(interval);
};
}, [status]);
useEffect(() => {
if (!progress || status !== "analysing") { if (!progress || status !== "analysing") {
anchorRef.current = null; anchorRef.current = null;
return; return;
@@ -90,14 +81,28 @@ export default function ProgressIndicator({
elapsedMs: progress.elapsedMs, elapsedMs: progress.elapsedMs,
timestamp: Date.now(), timestamp: Date.now(),
}; };
}, [progress?.elapsedMs, status]); }, [progress, status]);
const baseElapsedMs = progress?.elapsedMs ?? 0; useEffect(() => {
const interval = window.setInterval(() => {
const currentStatus = statusRef.current;
const currentProgress = progressRef.current;
const anchor = anchorRef.current; const anchor = anchorRef.current;
const elapsedMs = const baseElapsedMs = currentProgress?.elapsedMs ?? 0;
status === "analysing" && progress && anchor const nextElapsedMs =
? anchor.elapsedMs + Math.max(0, nowMs - anchor.timestamp) currentStatus === "analysing" && currentProgress && anchor
? anchor.elapsedMs + Math.max(0, Date.now() - anchor.timestamp)
: baseElapsedMs; : baseElapsedMs;
setElapsedMs((previous) =>
previous === nextElapsedMs ? previous : nextElapsedMs,
);
}, 1000);
return () => {
window.clearInterval(interval);
};
}, []);
const elapsedSeconds = Math.floor(elapsedMs / 1000); const elapsedSeconds = Math.floor(elapsedMs / 1000);
const remainingSeconds = Math.max(0, timeoutSeconds - elapsedSeconds); const remainingSeconds = Math.max(0, timeoutSeconds - elapsedSeconds);
const percentage = progress ? Math.round(progress.percentage) : 0; const percentage = progress ? Math.round(progress.percentage) : 0;