Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ concurrency:
cancel-in-progress: true

jobs:
# ── Backend: lint + format ─────────────────────────────────────────────
# ── Backend: lint + format + typecheck ───────────────────────────────
backend-lint:
name: "Backend · Lint & Format"
name: "Backend · Lint & Format & Typecheck"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -22,23 +22,48 @@ jobs:
enable-cache: true
- run: uv run --only-group lint ruff check core/ api/ db/ tests/
- run: uv run --only-group lint ruff format --check core/ api/ db/ tests/
- run: uv sync --frozen --group lint --group test
- run: uv run pyright core/ api/ db/

# ── Backend: tests ─────────────────────────────────────────────────────
# ── Backend: tests (with PostgreSQL) ─────────────────────────────────
backend-test:
name: "Backend · Tests"
runs-on: ubuntu-latest
needs: backend-lint

services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: data_agent
POSTGRES_PASSWORD: data_agent
POSTGRES_DB: data_agent
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U data_agent"
--health-interval 5s
--health-timeout 5s
--health-retries 5

env:
DATABASE_URL: postgresql+asyncpg://data_agent:data_agent@localhost:5432/data_agent

steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- run: uv sync --frozen --group test
- name: Check Alembic migration heads
run: uv run alembic heads | grep -c head | xargs test 1 -eq
- name: Apply Alembic migrations
run: uv run alembic upgrade head
- run: uv run pytest tests/ -v --tb=short

# ── Frontend: lint + typecheck ─────────────────────────────────────────
frontend-lint:
name: "Frontend · Lint & Typecheck"
# ── Frontend: lint + typecheck + tests ───────────────────────────────
frontend-checks:
name: "Frontend · Lint & Typecheck & Tests"
runs-on: ubuntu-latest
defaults:
run:
Expand All @@ -53,8 +78,9 @@ jobs:
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test

# ── Docker builds (parallel) ───────────────────────────────────────────
# ── Docker builds (parallel) ───────────────────────────────────────
docker-backend:
name: "Docker · Backend"
runs-on: ubuntu-latest
Expand All @@ -74,7 +100,7 @@ jobs:
docker-frontend:
name: "Docker · Frontend"
runs-on: ubuntu-latest
needs: frontend-lint
needs: frontend-checks
steps:
- uses: actions/checkout@v4
- run: >
Expand Down
5 changes: 4 additions & 1 deletion db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@


class Base(DeclarativeBase):
pass
# Use RETURNING to fetch server-generated values (server_default, onupdate)
# immediately after INSERT/UPDATE. Prevents MissingGreenlet errors when
# accessing these columns from sync code after an async flush().
__mapper_args__ = {"eager_defaults": True}


class ProjectRow(Base):
Expand Down
30 changes: 30 additions & 0 deletions frontend/__tests__/tool-meta.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest'
import { TOOL_META, HIDDEN_TOOLS } from '@/lib/tool-meta'

describe('TOOL_META', () => {
it('has active and done strings for each tool', () => {
for (const [key, meta] of Object.entries(TOOL_META)) {
expect(meta.active, `${key}.active`).toBeTruthy()
expect(meta.done, `${key}.done`).toBeTruthy()
expect(typeof meta.active).toBe('string')
expect(typeof meta.done).toBe('string')
}
})

it('contains the core tools', () => {
expect(TOOL_META).toHaveProperty('execute_python')
expect(TOOL_META).toHaveProperty('list_sources')
})
})

describe('HIDDEN_TOOLS', () => {
it('contains ask_question', () => {
expect(HIDDEN_TOOLS.has('ask_question')).toBe(true)
})

it('every hidden tool has metadata', () => {
for (const tool of HIDDEN_TOOLS) {
expect(TOOL_META, `${tool} missing from TOOL_META`).toHaveProperty(tool)
}
})
})
59 changes: 59 additions & 0 deletions frontend/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { cn, timeAgo } from '@/lib/utils'

describe('cn', () => {
it('merges class names', () => {
expect(cn('px-2', 'py-1')).toBe('px-2 py-1')
})

it('resolves tailwind conflicts (last wins)', () => {
expect(cn('px-2', 'px-4')).toBe('px-4')
})

it('handles conditional classes', () => {
expect(cn('base', false && 'hidden', 'extra')).toBe('base extra')
})

it('returns empty string for no inputs', () => {
expect(cn()).toBe('')
})
})

describe('timeAgo', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-03-18T12:00:00Z'))
})

afterEach(() => {
vi.useRealTimers()
})

it('returns "just now" for < 60 seconds', () => {
expect(timeAgo(new Date('2026-03-18T11:59:30Z'))).toBe('just now')
})

it('returns minutes ago', () => {
expect(timeAgo(new Date('2026-03-18T11:55:00Z'))).toBe('5m ago')
})

it('returns hours ago', () => {
expect(timeAgo(new Date('2026-03-18T09:00:00Z'))).toBe('3h ago')
})

it('returns days ago', () => {
expect(timeAgo(new Date('2026-03-15T12:00:00Z'))).toBe('3d ago')
})

it('returns months ago', () => {
expect(timeAgo(new Date('2025-12-18T12:00:00Z'))).toBe('3mo ago')
})

it('returns years ago', () => {
expect(timeAgo(new Date('2024-01-01T00:00:00Z'))).toBe('2y ago')
})

it('accepts string dates', () => {
expect(timeAgo('2026-03-18T11:50:00Z')).toBe('10m ago')
})
})
Loading
Loading