From e0d5a2e55747dcd6710d505b1ba66f2be1e3e228 Mon Sep 17 00:00:00 2001
From: Kishal
Date: Sat, 13 Jun 2026 22:47:06 +0530
Subject: [PATCH 1/5] feat: add global keyboard shortcuts modal (fixes #420)
---
frontend/src/components/chat/ChatPanel.tsx | 162 +-----------------
frontend/src/components/layout/Header.tsx | 3 +
.../layout/KeyboardShortcutsModal.tsx | 70 ++++++++
3 files changed, 74 insertions(+), 161 deletions(-)
create mode 100644 frontend/src/components/layout/KeyboardShortcutsModal.tsx
diff --git a/frontend/src/components/chat/ChatPanel.tsx b/frontend/src/components/chat/ChatPanel.tsx
index 9ea14be..bd66c8b 100644
--- a/frontend/src/components/chat/ChatPanel.tsx
+++ b/frontend/src/components/chat/ChatPanel.tsx
@@ -94,8 +94,6 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
const [isRecording, setIsRecording] = useState(false);
const [speechError, setSpeechError] = useState(null);
- // New State for Keyboard Shortcuts Help Modal
- const [showHelpModal, setShowHelpModal] = useState(false);
const recognitionRef = useRef(null);
const initialInputRef = useRef("");
@@ -547,8 +545,6 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
} else if (document.activeElement === textareaRef.current) {
e.preventDefault();
setInput("");
- } else if (showHelpModal) {
- setShowHelpModal(false);
} else if (showExportMenu) {
setShowExportMenu(false);
}
@@ -560,11 +556,6 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
textareaRef.current?.focus();
}
- // Shortcut 4: Ctrl/Cmd + / → Toggle shortcuts help modal
- if (isCmdOrCtrl && e.key === "/") {
- e.preventDefault();
- setShowHelpModal((prev) => !prev);
- }
// Shortcut 5: Ctrl/Cmd + Shift + C → Clear chat history
if (isCmdOrCtrl && e.shiftKey && (e.key === "c" || e.key === "C")) {
@@ -594,7 +585,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
window.removeEventListener("keydown", handleGlobalKeyDown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [input, streaming, showHelpModal, showExportMenu, messages]); // Dependencies updated to capture fresh state data
+ }, [input, streaming, showExportMenu, messages]); // Dependencies updated to capture fresh state data
return (
@@ -783,19 +774,6 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
)}
- {/* NEW Keyboard Shortcuts Info Button */}
-
@@ -892,144 +870,6 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
Ctrl+Enter to send from anywhere. Press Escape to cancel streaming.
-
- {/* ── NEW KEYBOARD SHORTCUTS HELP MODAL OVERLAY ───────────────── */}
- {showHelpModal && (
- setShowHelpModal(false)}
- >
-
e.stopPropagation()} // Stop overlay closing when clicking inside
- >
-
-
-
- ⌨️ Keyboard Shortcuts
-
-
- Enhance your typing productivity
-
-
-
-
- -
-
- Send Message
-
-
-
- Ctrl
-
- +
-
- Enter
-
-
-
- -
-
- Cancel Streaming / Clear Input
-
-
-
- Esc
-
-
-
- -
-
- Focus Chat Input
-
-
-
- Ctrl
-
- +
-
- K
-
-
-
- -
-
- Toggle Shortcuts Help
-
-
-
- Ctrl
-
- +
-
- /
-
-
-
- -
-
- Clear Chat History
-
-
-
- Ctrl
-
- +
-
- Shift
-
- +
-
- C
-
-
-
- -
-
- Toggle Export Menu
-
-
-
- Ctrl
-
- +
-
- Shift
-
- +
-
- E
-
-
-
- -
-
- Toggle Mic Recording
-
-
-
- Ctrl
-
- +
-
- Shift
-
- +
-
- M
-
-
-
-
-
-
- )}
);
}
diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx
index 178af94..ec958f2 100644
--- a/frontend/src/components/layout/Header.tsx
+++ b/frontend/src/components/layout/Header.tsx
@@ -33,6 +33,7 @@ import { api } from "@/lib/api";
import { useTheme } from "next-themes";
import { useSyncExternalStore } from "react";
+import { KeyboardShortcutsModal } from "./KeyboardShortcutsModal";
interface HeaderProps {
sidebarOpen: boolean;
@@ -164,6 +165,8 @@ export default function Header({
)}
+
+
{/* Workspace switcher */}
{
+ const handleKeyDown = (e: KeyboardEvent) => {
+ // Don't trigger if user is typing in an input or textarea
+ if (
+ e.target instanceof HTMLInputElement ||
+ e.target instanceof HTMLTextAreaElement ||
+ e.target instanceof HTMLSelectElement ||
+ (e.target as HTMLElement).isContentEditable
+ ) {
+ return;
+ }
+
+ if (e.key === "?") {
+ e.preventDefault();
+ setOpen((prev) => !prev);
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, []);
+
+ return (
+
+ );
+}
From 7929743c4a6d1db16f2a3f78a3717d8c47ebe9a8 Mon Sep 17 00:00:00 2001
From: Kishal
Date: Sat, 13 Jun 2026 22:47:16 +0530
Subject: [PATCH 2/5] fix: resolve psycopg driver and schema mismatches
preventing docker startup
---
backend/app/models.py | 4 ++--
docker-compose.yml | 16 ++++++----------
2 files changed, 8 insertions(+), 12 deletions(-)
diff --git a/backend/app/models.py b/backend/app/models.py
index 058cf84..0d3d9d6 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -203,11 +203,11 @@ class ApiKey(Base):
class WorkspaceInvitation(Base):
__tablename__ = "workspace_invitations"
- id = Column(String, primary_key=True, default=generate_uuid)
+ id = Column(GUID, primary_key=True, default=uuid.uuid4)
email = Column(String(120), nullable=False, index=True)
token_hash = Column(String(255), nullable=False, unique=True, index=True)
inviter_id = Column(
- String,
+ GUID,
ForeignKey("users.id"),
nullable=False,
index=True,
diff --git a/docker-compose.yml b/docker-compose.yml
index b587c4d..4638aa8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -57,7 +57,7 @@ services:
environment:
- SECRET_KEY=${SECRET_KEY:-dev-secret-key-change-me}
- HF_TOKEN=${HF_TOKEN}
- - DATABASE_URL=postgresql://${POSTGRES_USER:-pdf_rag_user}:${POSTGRES_PASSWORD:-pdf_rag_pass}@postgres:5432/${POSTGRES_DB:-pdf_rag}
+ - DATABASE_URL=postgresql+psycopg://${POSTGRES_USER:-pdf_rag_user}:${POSTGRES_PASSWORD:-pdf_rag_pass}@postgres:5432/${POSTGRES_DB:-pdf_rag}
- UPLOAD_DIR=/app/data/uploads
- CHROMA_PERSIST_DIR=/app/data/chroma_db
- GRAPH_PERSIST_DIR=/app/data/graphs
@@ -90,7 +90,7 @@ services:
environment:
- SECRET_KEY=${SECRET_KEY:-dev-secret-key-change-me}
- HF_TOKEN=${HF_TOKEN}
- - DATABASE_URL=postgresql://${POSTGRES_USER:-pdf_rag_user}:${POSTGRES_PASSWORD:-pdf_rag_pass}@postgres:5432/${POSTGRES_DB:-pdf_rag}
+ - DATABASE_URL=postgresql+psycopg://${POSTGRES_USER:-pdf_rag_user}:${POSTGRES_PASSWORD:-pdf_rag_pass}@postgres:5432/${POSTGRES_DB:-pdf_rag}
- UPLOAD_DIR=/app/data/uploads
- CHROMA_PERSIST_DIR=/app/data/chroma_db
- GRAPH_PERSIST_DIR=/app/data/graphs
@@ -123,15 +123,13 @@ services:
container_name: pdf_rag_worker
profiles: ["cpu"]
logging: *default-logging
- command: >
- sh -c "cd /app/backend &&
- celery -A app.celery_app.celery_app worker --loglevel=info"
+ command: celery -A app.celery_app.celery_app worker --loglevel=info
volumes:
- app_data:/app/data
environment:
- SECRET_KEY=${SECRET_KEY:-dev-secret-key-change-me}
- HF_TOKEN=${HF_TOKEN}
- - DATABASE_URL=postgresql://${POSTGRES_USER:-pdf_rag_user}:${POSTGRES_PASSWORD:-pdf_rag_pass}@postgres:5432/${POSTGRES_DB:-pdf_rag}
+ - DATABASE_URL=postgresql+psycopg://${POSTGRES_USER:-pdf_rag_user}:${POSTGRES_PASSWORD:-pdf_rag_pass}@postgres:5432/${POSTGRES_DB:-pdf_rag}
- UPLOAD_DIR=/app/data/uploads
- CHROMA_PERSIST_DIR=/app/data/chroma_db
- GRAPH_PERSIST_DIR=/app/data/graphs
@@ -151,15 +149,13 @@ services:
container_name: pdf_rag_worker
profiles: ["gpu"]
logging: *default-logging
- command: >
- sh -c "cd /app/backend &&
- celery -A app.celery_app.celery_app worker --loglevel=info"
+ command: celery -A app.celery_app.celery_app worker --loglevel=info
volumes:
- app_data:/app/data
environment:
- SECRET_KEY=${SECRET_KEY:-dev-secret-key-change-me}
- HF_TOKEN=${HF_TOKEN}
- - DATABASE_URL=postgresql://${POSTGRES_USER:-pdf_rag_user}:${POSTGRES_PASSWORD:-pdf_rag_pass}@postgres:5432/${POSTGRES_DB:-pdf_rag}
+ - DATABASE_URL=postgresql+psycopg://${POSTGRES_USER:-pdf_rag_user}:${POSTGRES_PASSWORD:-pdf_rag_pass}@postgres:5432/${POSTGRES_DB:-pdf_rag}
- UPLOAD_DIR=/app/data/uploads
- CHROMA_PERSIST_DIR=/app/data/chroma_db
- GRAPH_PERSIST_DIR=/app/data/graphs
From 5a0a9bb377954c727dd88f33cf0cdd173cfc686f Mon Sep 17 00:00:00 2001
From: Kishal
Date: Sat, 13 Jun 2026 23:04:11 +0530
Subject: [PATCH 3/5] fix: resolve CI failures (duplicate TS import and missing
pymupdf4llm)
---
backend/requirements.txt | 2 ++
frontend/src/components/layout/Header.tsx | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/backend/requirements.txt b/backend/requirements.txt
index f46463b..a7f5f5d 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -29,6 +29,8 @@ httpx
# Document Processing
PyMuPDF
+pymupdf4llm
+google-generativeai
pdfplumber
python-docx
unstructured[pdf]
diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx
index aa69816..c6e0671 100644
--- a/frontend/src/components/layout/Header.tsx
+++ b/frontend/src/components/layout/Header.tsx
@@ -38,7 +38,7 @@ import {
} from "@/components/ui/dialog";
import { useWorkspaceStore, WORKSPACES, type WorkspaceId } from "@/store/workspace-store";
import { api } from "@/lib/api";
-import { useSyncExternalStore } from "react";
+
import { KeyboardShortcutsModal } from "./KeyboardShortcutsModal";
interface HeaderProps {
From 45c8cbb03f7796594cc717787b4ee7fe8e1c1eaa Mon Sep 17 00:00:00 2001
From: Kishal
Date: Sat, 13 Jun 2026 23:31:07 +0530
Subject: [PATCH 4/5] fix(ci): resolve backend import error & frontend prettier
issues
- Add google-genai to requirements.txt for new Google Gen AI SDK (v2+)
- Fix prettier formatting on 5 modified frontend files
---
backend/app/services/layout_parser.py | 13 +-
backend/app/tasks.py | 4 +-
backend/requirements.txt | 1 +
backend/tests/test_celery_ingestion.py | 12 +-
frontend/src/app/register/page.tsx | 32 +++-
frontend/src/components/chat/ChatPanel.tsx | 169 ++++++++++++-----
frontend/src/components/layout/Header.tsx | 71 +++++---
.../layout/KeyboardShortcutsModal.tsx | 28 ++-
frontend/src/lib/__tests__/api.test.ts | 172 ++++++++++--------
9 files changed, 332 insertions(+), 170 deletions(-)
diff --git a/backend/app/services/layout_parser.py b/backend/app/services/layout_parser.py
index dbec95e..faac372 100644
--- a/backend/app/services/layout_parser.py
+++ b/backend/app/services/layout_parser.py
@@ -3,12 +3,6 @@
import fitz # PyMuPDF
import pymupdf4llm
-from google import (
- genai, # Since the repo uses Gemini, we'll swap to Gemini 2.5 Flash for vision tasks!
-)
-
-# Initialize Gemini Client
-client = genai.Client()
class AdvancedPDFParser:
@@ -49,6 +43,13 @@ def process_embedded_images(self, page_num: int, page_obj: fitz.Page) -> List[st
image_descriptions = []
image_list = page_obj.get_images(full=True)
+ try:
+ from google import genai
+ client = genai.Client()
+ except Exception as e:
+ print(f"Gemini client init failed, skipping vision: {e}")
+ return image_descriptions
+
for img_index, img in enumerate(image_list):
xref = img[0]
base_image = self.doc.extract_image(xref)
diff --git a/backend/app/tasks.py b/backend/app/tasks.py
index 811cad0..ef0c7ff 100644
--- a/backend/app/tasks.py
+++ b/backend/app/tasks.py
@@ -84,11 +84,11 @@ def process_document(
pass
# 4. Mark document pipeline processing as completely successful
- doc.status = "completed"
+ doc.status = "ready"
doc.processing_progress = 100
db.commit()
- return {"document_id": document_id, "status": "completed"}
+ return {"document_id": document_id, "status": "ready"}
except Exception as exc:
logger.error("Document %s processing failed (attempt %s): %s", document_id, self.request.retries + 1, exc)
diff --git a/backend/requirements.txt b/backend/requirements.txt
index a7f5f5d..78135df 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -31,6 +31,7 @@ httpx
PyMuPDF
pymupdf4llm
google-generativeai
+google-genai
pdfplumber
python-docx
unstructured[pdf]
diff --git a/backend/tests/test_celery_ingestion.py b/backend/tests/test_celery_ingestion.py
index 2e359e6..60cfae1 100644
--- a/backend/tests/test_celery_ingestion.py
+++ b/backend/tests/test_celery_ingestion.py
@@ -27,18 +27,22 @@ def test_process_document_ingestion_pipeline(db_session):
mock_session_factory.return_value.__enter__.return_value = db_session
mock_session_factory.return_value = db_session
- # Patch the factory globally, and patch ingest_document right where app.tasks calls it
+ # Patch the factory globally, and patch AdvancedPDFParser constructor and ingest_document
+ # right where app.tasks calls it
with patch("app.database.SessionLocal", mock_session_factory, create=True), \
patch("app.services.document_ingestion.SessionLocal", mock_session_factory, create=True), \
- patch("app.tasks.ingest_document") as mock_ingest:
-
+ patch("app.tasks.AdvancedPDFParser.__init__", return_value=None) as mock_init, \
+ patch("app.tasks.AdvancedPDFParser.ingest_document") as mock_ingest:
+
# Simulate what the underlying service does upon a successful processing run
def simulate_successful_ingestion(*args, **kwargs):
doc = db_session.query(Document).filter_by(id="test-doc-123").first()
if doc:
doc.status = "ready"
db_session.commit()
- return {"status": "success"}
+ return [
+ {"page_number": 1, "text": "Sample text", "type": "text_layout"}
+ ]
mock_ingest.side_effect = simulate_successful_ingestion
diff --git a/frontend/src/app/register/page.tsx b/frontend/src/app/register/page.tsx
index 6f0e1a2..3872b4a 100644
--- a/frontend/src/app/register/page.tsx
+++ b/frontend/src/app/register/page.tsx
@@ -6,7 +6,13 @@ import { useAuth } from "@/lib/auth";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
-import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+} from "@/components/ui/card";
import { Brain } from "lucide-react";
import Link from "next/link";
import GoogleSignInButton from "@/components/auth/GoogleSignInButton";
@@ -26,7 +32,6 @@ export default function RegisterPage() {
const [verificationUrl, setVerificationUrl] = useState("");
const [loading, setLoading] = useState(false);
-
// Redirect if already logged in
useEffect(() => {
if (initialized && user) {
@@ -51,7 +56,8 @@ export default function RegisterPage() {
setSuccess(result.message);
setVerificationUrl(result.verification_url ?? "");
} catch (err: unknown) {
- const message = err instanceof Error ? err.message : t("register.fallbackError");
+ const message =
+ err instanceof Error ? err.message : t("register.fallbackError");
setError(message);
} finally {
setLoading(false);
@@ -69,7 +75,9 @@ export default function RegisterPage() {
- {t("register.title")}
+
+ {t("register.title")}
+
{t("register.description")}
@@ -98,7 +106,10 @@ export default function RegisterPage() {
{t("register.verifyMessage", { email: registeredEmail })}
{verificationUrl && (
-
+
{t("register.openVerification")}
)}
@@ -146,7 +157,11 @@ export default function RegisterPage() {
disabled={Boolean(success)}
/>
-
+ }
+ />
Keyboard Shortcuts
@@ -50,7 +68,9 @@ export function KeyboardShortcutsModal() {
{SHORTCUTS.map((shortcut, idx) => (
-
{shortcut.description}
+
+ {shortcut.description}
+
{shortcut.keys.map((key, i) => (
{
+describe("ApiClient", () => {
let fetchMock: ReturnType;
let localStorageStore: Record = {};
@@ -22,11 +22,15 @@ describe('ApiClient', () => {
localStorageStore = {};
}),
};
- Object.defineProperty(global, 'window', {
- value: { localStorage: mockLocalStorage, dispatchEvent: vi.fn(), CustomEvent: class {} },
+ Object.defineProperty(global, "window", {
+ value: {
+ localStorage: mockLocalStorage,
+ dispatchEvent: vi.fn(),
+ CustomEvent: class {},
+ },
writable: true,
});
- Object.defineProperty(global, 'localStorage', {
+ Object.defineProperty(global, "localStorage", {
value: mockLocalStorage,
writable: true,
});
@@ -36,118 +40,136 @@ describe('ApiClient', () => {
vi.restoreAllMocks();
});
- describe('Headers & Auth', () => {
- it('should include Authorization header if token exists in localStorage', async () => {
- localStorageStore['token'] = 'dummy_auth_token_string';
- fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ ok: true })));
-
- await api.get('/test');
-
+ describe("Headers & Auth", () => {
+ it("should include Authorization header if token exists in localStorage", async () => {
+ localStorageStore["token"] = "dummy_auth_token_string";
+ fetchMock.mockResolvedValueOnce(
+ new Response(JSON.stringify({ ok: true })),
+ );
+
+ await api.get("/test");
+
expect(fetchMock).toHaveBeenCalledWith(
`${API_BASE}/test`,
expect.objectContaining({
- method: 'GET',
+ method: "GET",
headers: expect.objectContaining({
- 'Content-Type': 'application/json',
- 'Authorization': 'Bearer dummy_auth_token_string',
- })
- })
+ "Content-Type": "application/json",
+ Authorization: "Bearer dummy_auth_token_string",
+ }),
+ }),
);
});
- it('should NOT include Authorization header if token is missing', async () => {
- fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ ok: true })));
-
- await api.get('/test');
-
+ it("should NOT include Authorization header if token is missing", async () => {
+ fetchMock.mockResolvedValueOnce(
+ new Response(JSON.stringify({ ok: true })),
+ );
+
+ await api.get("/test");
+
const calls = fetchMock.mock.calls;
const headers = calls[0][1].headers;
- expect(headers).not.toHaveProperty('Authorization');
+ expect(headers).not.toHaveProperty("Authorization");
});
});
- describe('Parameter Handling', () => {
- it('should stringify JSON bodies correctly in POST requests', async () => {
- fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ success: true })));
- const body = { document_id: '123', query: 'hello' };
-
- await api.post('/message', body);
-
+ describe("Parameter Handling", () => {
+ it("should stringify JSON bodies correctly in POST requests", async () => {
+ fetchMock.mockResolvedValueOnce(
+ new Response(JSON.stringify({ success: true })),
+ );
+ const body = { document_id: "123", query: "hello" };
+
+ await api.post("/message", body);
+
expect(fetchMock).toHaveBeenCalledWith(
`${API_BASE}/message`,
expect.objectContaining({
- method: 'POST',
- body: JSON.stringify(body)
- })
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
);
});
- it('should handle FormData correctly in postForm without overriding Content-Type', async () => {
- fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ success: true })));
+ it("should handle FormData correctly in postForm without overriding Content-Type", async () => {
+ fetchMock.mockResolvedValueOnce(
+ new Response(JSON.stringify({ success: true })),
+ );
const formData = new FormData();
- formData.append('file', new Blob(['test'], { type: 'text/plain' }), 'test.txt');
-
- await api.postForm('/upload', formData);
-
+ formData.append(
+ "file",
+ new Blob(["test"], { type: "text/plain" }),
+ "test.txt",
+ );
+
+ await api.postForm("/upload", formData);
+
const callArgs = fetchMock.mock.calls[0];
const reqInit = callArgs[1];
-
- expect(reqInit.method).toBe('POST');
+
+ expect(reqInit.method).toBe("POST");
expect(reqInit.body).toBe(formData);
- expect(reqInit.headers).not.toHaveProperty('Content-Type');
+ expect(reqInit.headers).not.toHaveProperty("Content-Type");
});
});
- describe('Error Handling', () => {
- it('should throw connection error message if fetch throws TypeError', async () => {
- fetchMock.mockRejectedValueOnce(new TypeError('Failed to fetch'));
-
- await expect(api.get('/test')).rejects.toThrow(CONNECTION_ERROR_MESSAGE);
+ describe("Error Handling", () => {
+ it("should throw connection error message if fetch throws TypeError", async () => {
+ fetchMock.mockRejectedValueOnce(new TypeError("Failed to fetch"));
+
+ await expect(api.get("/test")).rejects.toThrow(CONNECTION_ERROR_MESSAGE);
});
- it('should throw parsed error message if response is not ok', async () => {
+ it("should throw parsed error message if response is not ok", async () => {
fetchMock.mockResolvedValueOnce(
- new Response(JSON.stringify({ detail: 'Invalid document ID' }), {
+ new Response(JSON.stringify({ detail: "Invalid document ID" }), {
status: 400,
- statusText: 'Bad Request'
- })
+ statusText: "Bad Request",
+ }),
);
-
- await expect(api.get('/test')).rejects.toThrow('Invalid document ID');
+
+ await expect(api.get("/test")).rejects.toThrow("Invalid document ID");
});
- it('should fallback to statusText if response has no JSON body', async () => {
+ it("should fallback to statusText if response has no JSON body", async () => {
fetchMock.mockResolvedValueOnce(
new Response(null, {
status: 500,
- statusText: 'Internal Server Error'
- })
+ statusText: "Internal Server Error",
+ }),
);
-
- await expect(api.get('/test')).rejects.toThrow('Internal Server Error');
+
+ await expect(api.get("/test")).rejects.toThrow("Internal Server Error");
});
});
- describe('Token Refresh', () => {
- it('should auto-refresh token on 401 response', async () => {
- localStorageStore['token'] = 'old_dummy_token';
- localStorageStore['refresh_token'] = 'dummy_refresh_string';
-
+ describe("Token Refresh", () => {
+ it("should auto-refresh token on 401 response", async () => {
+ localStorageStore["token"] = "old_dummy_token";
+ localStorageStore["refresh_token"] = "dummy_refresh_string";
+
// 1st request -> 401
fetchMock.mockResolvedValueOnce(new Response(null, { status: 401 }));
-
+
// Refresh request -> 200
- fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
- access_token: 'new_dummy_token'
- })));
-
+ fetchMock.mockResolvedValueOnce(
+ new Response(
+ JSON.stringify({
+ access_token: "new_dummy_token",
+ }),
+ ),
+ );
+
// Retry original request -> 200
- fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ data: 'success' })));
-
- const res = await api.get('/protected');
-
- expect(res).toEqual({ data: 'success' });
- expect(localStorageStore['token']).toBe('new_dummy_token');
+ fetchMock.mockResolvedValueOnce(
+ new Response(JSON.stringify({ data: "success" })),
+ );
+
+ const res = await api.get("/protected");
+
+ expect(res).toEqual({ data: "success" });
+ expect(localStorageStore["token"]).toBe("new_dummy_token");
expect(fetchMock).toHaveBeenCalledTimes(3);
});
});
From ba9fe1ff1e3b686626b5bb8c70fa7d97e0905ff6 Mon Sep 17 00:00:00 2001
From: Kishal
Date: Sun, 14 Jun 2026 20:00:07 +0530
Subject: [PATCH 5/5] fix: resolve backend test failures (Prometheus
AttributeError and ingestion status)
---
backend/app/observability.py | 28 +++++++++++++++++++++++++-
backend/tests/test_celery_ingestion.py | 2 +-
2 files changed, 28 insertions(+), 2 deletions(-)
diff --git a/backend/app/observability.py b/backend/app/observability.py
index 662cf75..e2cba9b 100644
--- a/backend/app/observability.py
+++ b/backend/app/observability.py
@@ -9,7 +9,33 @@
from fastapi import FastAPI
from prometheus_client import Gauge
-from prometheus_fastapi_instrumentator import Instrumentator
+from prometheus_fastapi_instrumentator import Instrumentator, routing
+from starlette.routing import Match
+
+# ── Workaround for FastAPI 0.135+ and prometheus-fastapi-instrumentator 8.0.0 ──
+# Newer FastAPI versions include _IncludedRouter objects in app.routes which
+# lack a '.path' attribute, causing AttributeErrors during instrumentation.
+def _patched_get_route_name(scope, routes, route_name=None):
+ """Safe version of _get_route_name that handles routes without a .path attribute."""
+ for route in routes:
+ try:
+ match, child_scope = route.matches(scope)
+ except Exception:
+ continue
+
+ if match == Match.FULL:
+ # If we have a full match and the route has a path, use it and return early.
+ # This matches Starlette's behavior where the first matching route wins.
+ if hasattr(route, "path"):
+ return route.path
+ elif match == Match.PARTIAL and hasattr(route, "routes"):
+ # Recursive call for nested routes (e.g. Mounts)
+ route_name = _patched_get_route_name(child_scope, route.routes, route_name)
+ if route_name:
+ return route_name
+ return route_name
+
+routing._get_route_name = _patched_get_route_name
APP_PROCESS_RSS_BYTES = Gauge(
"app_process_resident_memory_bytes",
diff --git a/backend/tests/test_celery_ingestion.py b/backend/tests/test_celery_ingestion.py
index e5727e7..9bc38d4 100644
--- a/backend/tests/test_celery_ingestion.py
+++ b/backend/tests/test_celery_ingestion.py
@@ -48,4 +48,4 @@ def test_process_document_ingestion_pipeline(db_session):
# Query the database to verify the state update
updated_doc = db_session.query(Document).filter_by(id="test-doc-123").first()
assert updated_doc is not None
- assert updated_doc.status == "completed"
\ No newline at end of file
+ assert updated_doc.status == "ready"
\ No newline at end of file