Skip to content

Commit e8b92f9

Browse files
authored
ci: GitHub Actions for backend, UI, and Docker build (#6)
* chore: fix ruff unused-import errors * style: remove stray blank line in test_generate_router_dataset.py * fix: resolve pre-existing tsc and eslint errors in UI * ci: add GitHub Actions workflow (backend + ui + docker build) * ci: fix hidden network dependency in backend tests - Several rag-chat endpoint tests didn't mock enqueue_memory_refresh, a fire-and-forget outbox write. They only passed locally because the (gitignored) .env pointed at a real Supabase project; in CI there's no such project, so the write failed. Mock it by default in conftest.py; tests that care about the call still patch/assert it explicitly. - Backend Test step now sets dummy SUPABASE_URL/SECRET_KEY/JWKS_URL env vars so Settings' "is this configured" checks pass without a real project. example.com is used because it actually resolves via DNS (unlike a fabricated *.supabase.co host), even though invalid JWTs fail parsing before any network call is made. * test: fix flaky race on fire-and-forget Neo4j persistence task _execute_research_run schedules Neo4j persistence via asyncio.create_task, intentionally decoupled from run completion. The test asserted on it right after asyncio.run() returned, racing the background task — it usually won locally but lost more often on the slower/loaded GitHub Actions runner. Drain pending tasks before asserting so the test is deterministic. * ci: add job timeouts and clarify non-blocking mypy step
1 parent c43b428 commit e8b92f9

8 files changed

Lines changed: 96 additions & 15 deletions

File tree

.github/workflows/ci.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
backend:
10+
runs-on: ubuntu-latest
11+
timeout-minutes: 15
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: astral-sh/setup-uv@v5
15+
with:
16+
enable-cache: true
17+
- name: Install
18+
run: uv sync --extra dev
19+
- name: Lint
20+
run: uv run ruff check src tests
21+
- name: Typecheck (non-blocking)
22+
# 38 pre-existing errors as of this workflow's introduction; tighten
23+
# once the backlog is cleared instead of blocking every PR on it.
24+
run: uv run mypy src || true
25+
- name: Test
26+
env:
27+
# Dummy values so config "is this configured?" checks pass; no real
28+
# network calls are made in the mocked test suite (see auth.py /
29+
# supabase_store.py — invalid tokens fail parsing before any
30+
# request reaches these URLs).
31+
SUPABASE_URL: https://example.com
32+
SUPABASE_SECRET_KEY: dummy-secret-key-for-ci
33+
SUPABASE_JWKS_URL: https://example.com/auth/v1/.well-known/jwks.json
34+
run: uv run python -m pytest tests/ -q
35+
36+
ui:
37+
runs-on: ubuntu-latest
38+
timeout-minutes: 15
39+
defaults:
40+
run:
41+
working-directory: ui
42+
steps:
43+
- uses: actions/checkout@v4
44+
- uses: actions/setup-node@v4
45+
with:
46+
node-version: 22
47+
cache: npm
48+
cache-dependency-path: ui/package-lock.json
49+
- name: Install
50+
run: npm ci
51+
- name: Lint
52+
run: npx eslint src --max-warnings=0
53+
- name: Typecheck
54+
run: npx tsc --noEmit -p tsconfig.app.json
55+
- name: Test
56+
run: npx vitest run
57+
58+
docker:
59+
runs-on: ubuntu-latest
60+
timeout-minutes: 15
61+
if: github.ref == 'refs/heads/main'
62+
needs: [backend, ui]
63+
steps:
64+
- uses: actions/checkout@v4
65+
- name: Build image
66+
run: docker build -t cortex:${{ github.sha }} .

src/api/rag_chat_helpers.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import asyncio
66
import logging
77
import time
8-
import uuid
98
from dataclasses import dataclass, field
109
from typing import TYPE_CHECKING, Any
1110

@@ -28,16 +27,13 @@
2827
from src.prompts.registry import prompt_registry
2928
from src.config import settings
3029
from src.rag import (
31-
RagChatMessage,
32-
append_chat_message,
3330
create_or_get_chat_session,
3431
create_or_get_workspace_chat_session,
3532
get_agent_for_chat,
3633
list_chat_messages as list_rag_chat_messages,
3734
list_rag_chat_session_attachments,
38-
list_ready_rag_chat_session_attachment_resource_ids,
35+
list_ready_rag_chat_session_attachment_resource_ids, # noqa: F401 - patched by tests/conftest.py
3936
list_workspace_ready_resource_ids,
40-
retrieve_context_for_query,
4137
retrieve_merged_context_for_agent_chat,
4238
)
4339
from src.rag_engine import RagQueryResult

tests/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ def mock_list_ready_session_attachment_resource_ids():
2525
yield
2626

2727

28+
@pytest.fixture(autouse=True)
29+
def mock_enqueue_memory_refresh():
30+
# ponytail: several endpoint tests don't mock this fire-and-forget outbox
31+
# write and previously only passed because a local .env pointed at a real
32+
# Supabase project. Default it to a no-op here; tests that care about the
33+
# call still patch/assert it explicitly (see test_api.py:1363).
34+
with patch(
35+
"src.api.endpoints.enqueue_memory_refresh",
36+
new=AsyncMock(return_value=False),
37+
):
38+
yield
39+
40+
2841
@pytest.fixture(autouse=True)
2942
def mock_list_session_attachments():
3043
with (

tests/finetune/test_generate_router_dataset.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
import json
33
from pathlib import Path
44

5-
import pytest
6-
75
from scripts.generate_router_dataset import deduplicate_inputs, split_records, write_jsonl
86

97
_ACTIONS = [

tests/test_api.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,14 +485,24 @@ def _mock_trace_ctx(**_kwargs):
485485
patch("src.api.endpoints.start_workflow_run", side_effect=_mock_trace_ctx),
486486
patch("src.api.endpoints.end_workflow_run"),
487487
):
488-
asyncio.run(
489-
_execute_research_run(
488+
489+
async def _run_and_drain() -> None:
490+
# ponytail: _execute_research_run fires Neo4j persistence via a
491+
# fire-and-forget asyncio.create_task (intentionally decoupled
492+
# from run completion). asyncio.run() only awaits the main
493+
# coroutine, so the background task's completion is a race —
494+
# drain pending tasks here so the assertion below is deterministic.
495+
await _execute_research_run(
490496
session_id="session-1",
491497
run_id="run-1",
492498
user_id="user-1",
493499
query="What is LangGraph?",
494500
)
495-
)
501+
pending = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
502+
if pending:
503+
await asyncio.gather(*pending)
504+
505+
asyncio.run(_run_and_drain())
496506

497507
assert any(
498508
call.kwargs.get("patch", {}).get("status") == "completed"

tests/test_dspy_optimizer.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,15 @@
22

33
from __future__ import annotations
44

5-
import json
65
from pathlib import Path
7-
from unittest.mock import MagicMock, PropertyMock, patch
6+
from unittest.mock import MagicMock, patch
87

98
import dspy
109
import pytest
1110

1211
from src.prompts.dspy_optimizer import (
1312
DspyPromptOptimizer,
1413
FollowupAnswerModule,
15-
OptimizedPrompt,
1614
OptimizationResult,
1715
RagChatSystemModule,
1816
ReportModule,

ui/src/components/chat/ChatThreadContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ export function ChatThreadContainer({
293293
async (files: File[]) => {
294294
if (files.length === 0 || !transport.uploadAttachments || !transport.ensureSession) return
295295

296-
const uploadIds = files.map(() => crypto.randomUUID())
296+
const uploadIds: string[] = files.map(() => crypto.randomUUID())
297297
setPendingUploads((prev) => [
298298
...prev,
299299
...files.map((file, index) => ({

ui/src/components/chat/transports.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export const workspaceChatTransport: ChatTransport = {
158158
deleteAttachment: async (sessionId, attachmentId, accessToken) => {
159159
await deleteRagWorkspaceChatSessionAttachment(sessionId, attachmentId, accessToken)
160160
},
161-
streamMessage: async (message, sessionId, accessToken, callbacks, tools, _files) => {
161+
streamMessage: async (message, sessionId, accessToken, callbacks, tools) => {
162162
await streamRagWorkspaceChat(message, sessionId, accessToken, {
163163
signal: callbacks.signal,
164164
onSession: callbacks.onSession,

0 commit comments

Comments
 (0)