Skip to content

Commit c301634

Browse files
committed
ci: add browser smoke test, AGENTS.md, and CLAUDE.md
Adds AGENTS.md + CLAUDE.md at the repo root with Studio-specific guidance for AI coding agents, and a PR-time browser smoke test that boots gunicorn behind nginx, logs in, and walks a curated set of in-app URLs collecting console errors and same-origin HTTP failures. The smoke test job consumes a built frontend artifact from the existing build_assets job (upload step added), mirroring Kolibri's whl -> browser_smoke_test pattern. nginx fronts gunicorn so the request path matches prod.
1 parent 402f22e commit c301634

5 files changed

Lines changed: 748 additions & 0 deletions

File tree

.github/workflows/deploytest.yml

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ jobs:
4545
pnpm rebuild node-sass
4646
- name: Build frontend
4747
run: pnpm run build
48+
- name: Upload frontend bundle
49+
uses: actions/upload-artifact@v7
50+
with:
51+
name: studio-frontend-bundle
52+
path: |
53+
contentcuration/contentcuration/static/studio/
54+
contentcuration/build/webpack-stats.json
55+
if-no-files-found: error
56+
retention-days: 1
4857
make_messages:
4958
name: Build all message files
5059
needs: pre_job
@@ -79,3 +88,146 @@ jobs:
7988
sudo apt-get install -y gettext
8089
- name: Test Django makemessages
8190
run: python contentcuration/manage.py makemessages --all
91+
browser_smoke_test:
92+
name: Browser smoke test
93+
needs: [pre_job, build_assets]
94+
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
95+
runs-on: ubuntu-latest
96+
timeout-minutes: 10
97+
services:
98+
postgres:
99+
image: postgres:16
100+
env:
101+
POSTGRES_USER: learningequality
102+
POSTGRES_PASSWORD: kolibri
103+
POSTGRES_DB: kolibri-studio
104+
options: >-
105+
--health-cmd pg_isready
106+
--health-interval 10s
107+
--health-timeout 5s
108+
--health-retries 5
109+
ports:
110+
- 5432:5432
111+
redis:
112+
image: redis:6.0.9
113+
options: >-
114+
--health-cmd "redis-cli ping"
115+
--health-interval 10s
116+
--health-timeout 5s
117+
--health-retries 5
118+
ports:
119+
- 6379:6379
120+
env:
121+
DJANGO_SETTINGS_MODULE: contentcuration.settings
122+
DATA_DB_HOST: localhost
123+
AWS_S3_ENDPOINT_URL: http://localhost:9000
124+
AWS_BUCKET_NAME: content
125+
CELERY_BROKER_ENDPOINT: localhost
126+
CELERY_REDIS_DB: "0"
127+
CELERY_REDIS_PASSWORD: ""
128+
# generate_storage_url() only handles k8s / docker-compose / unset — an
129+
# unrecognized value leaves the URL unbound (500). MinIO runs on :9000
130+
# here, which is exactly the docker-compose path.
131+
RUN_MODE: docker-compose
132+
SMOKE_EMAIL: smokeadmin@example.com
133+
SMOKE_PASSWORD: smokepass1234
134+
steps:
135+
- uses: actions/checkout@v7
136+
- name: Set up MinIO
137+
run: |
138+
docker run -d -p 9000:9000 --name minio \
139+
-e "MINIO_ROOT_USER=development" \
140+
-e "MINIO_ROOT_PASSWORD=development" \
141+
-e "MINIO_DEFAULT_BUCKETS=content:public" \
142+
bitnamilegacy/minio:2024.5.28
143+
- name: Install gettext (for compilemessages)
144+
run: |
145+
sudo apt-get update -y
146+
sudo apt-get install -y gettext
147+
- name: Install uv
148+
uses: astral-sh/setup-uv@v7
149+
with:
150+
python-version: '3.10'
151+
activate-environment: "true"
152+
enable-cache: "true"
153+
- name: Install python dependencies
154+
run: |
155+
uv pip sync requirements.txt
156+
# WhiteNoise lets gunicorn serve /static/ without nginx (see
157+
# integration_testing/smoke_wsgi.py). Smoke-test-only, so not in
158+
# requirements.txt.
159+
uv pip install "whitenoise<7"
160+
- name: Download frontend bundle
161+
uses: actions/download-artifact@v8
162+
with:
163+
name: studio-frontend-bundle
164+
# upload-artifact strips the least-common-ancestor of the uploaded
165+
# paths (here contentcuration/), so extract back under it: the bundle
166+
# must land at contentcuration/contentcuration/static/studio/ and the
167+
# stats file at contentcuration/build/webpack-stats.json (STATS_FILE).
168+
path: contentcuration
169+
- name: Cache Playwright browsers
170+
uses: actions/cache@v5
171+
with:
172+
path: ~/.cache/ms-playwright
173+
key: playwright-chromium-${{ runner.os }}-v1
174+
- name: Install Chromium and system deps
175+
run: uvx --from "playwright<2" playwright install --with-deps chromium
176+
- name: Prepare database
177+
run: |
178+
python contentcuration/manage.py migrate --noinput
179+
python contentcuration/manage.py loadconstants
180+
- name: Create smoke test user
181+
shell: python
182+
run: |
183+
import os
184+
import sys
185+
# contentcuration package lives one level down; put it on sys.path
186+
# so django.setup() can import contentcuration.settings.
187+
sys.path.insert(0, "contentcuration")
188+
import django
189+
django.setup()
190+
from django.contrib.auth import get_user_model
191+
# Studio's custom User model: create_superuser(email, first_name, last_name, password).
192+
# is_active defaults to False, so we have to flip it explicitly —
193+
# otherwise the user exists but can't log in.
194+
u = get_user_model().objects.create_superuser(
195+
os.environ["SMOKE_EMAIL"],
196+
"Smoke",
197+
"Admin",
198+
password=os.environ["SMOKE_PASSWORD"],
199+
)
200+
u.is_active = True
201+
u.save()
202+
- name: Prepare static and translations
203+
run: |
204+
python contentcuration/manage.py collectstatic --noinput
205+
cd contentcuration && python manage.py compilemessages
206+
- name: Start gunicorn (WhiteNoise serves /static/, no nginx)
207+
run: |
208+
nohup gunicorn integration_testing.smoke_wsgi:application \
209+
--pythonpath . \
210+
--timeout=120 --workers=1 --threads=1 \
211+
--bind=0.0.0.0:8080 --log-level=info \
212+
> "${{ runner.temp }}/gunicorn.log" 2>&1 &
213+
echo "gunicorn started in background"
214+
- name: Run browser smoke test
215+
# SCREENSHOT_DIR uses the runner context, which is only available in a
216+
# step env — not the job-level env.
217+
env:
218+
SCREENSHOT_DIR: ${{ runner.temp }}/smoke_test_screenshots
219+
run: uv run --script integration_testing/smoke_test.py
220+
- name: Upload screenshots
221+
if: always()
222+
uses: actions/upload-artifact@v7
223+
with:
224+
name: smoke_test_screenshots
225+
path: ${{ runner.temp }}/smoke_test_screenshots
226+
if-no-files-found: ignore
227+
- name: Upload gunicorn log on failure
228+
if: failure()
229+
uses: actions/upload-artifact@v7
230+
with:
231+
name: smoke_test_gunicorn_log
232+
path: ${{ runner.temp }}/gunicorn.log
233+
if-no-files-found: ignore

AGENTS.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<!-- Generic guidance for all coding agents (Claude Code, Zed, Cursor, etc.) -->
2+
3+
# Kolibri Studio Development Guide for AI Coding Agents
4+
5+
**Project:** Kolibri Studio — web app for authoring and publishing learning channels to Kolibri
6+
**Stack:** Python/Django backend, Vue.js 2.7 frontend, Kolibri Design System (KDS) + legacy Vuetify 1.5, Postgres/Redis/MinIO/Celery services, pytest/Jest testing
7+
**Platform:** web (server-deployed, not packaged for clients)
8+
9+
## Quick Start
10+
11+
```bash
12+
uv pip sync requirements.txt requirements-dev.txt # Python deps
13+
pnpm install # Node deps
14+
pre-commit install # Required — commits fail without this
15+
make dcservicesup # Bring up postgres / redis / minio in docker
16+
pnpm devsetup # Migrate + load sample data + create admin
17+
pnpm devserver # Django :8080 + Webpack watcher
18+
```
19+
20+
→ Full setup: `README.md` | Local production-shape stack: `make dcup` (uses `docker-compose.yml`)
21+
22+
## Critical Gotchas
23+
24+
### ⚠️ BEFORE Writing Any Vue Component, Search for Existing Ones
25+
26+
Do not create a new component without first searching for an existing solution:
27+
1. **Kolibri Design System** ([docs](https://design-system.learningequality.org/)) — `KButton`, `KCircularLoader`, `KTextbox`, `KSelect`, `KModal`, `KCheckbox`, `KIcon`, `KTable`, etc.
28+
2. **`contentcuration/contentcuration/frontend/shared/`** — Studio-specific shared components.
29+
3. **Vuetify 1.5** — for legacy widgets KDS doesn't cover (data tables, complex layout primitives). See the next gotcha before reaching for Vuetify in new code.
30+
31+
If a component does 80% of what you need, wrap it — do not rewrite.
32+
33+
### ⚠️ Use KDS, Not Vuetify, for New Code
34+
35+
KDS is the design-system source of truth. Vuetify is legacy and being phased out. **Do not introduce Vuetify into a fresh component** — new files use KDS. When you're already editing a file for another reason and a Vuetify widget in it has a KDS equivalent, migrating it is welcome — **keep it small and in-scope** (the component you're touching, not a sweep of the whole file). Don't open unrelated PRs solely to migrate, and don't half-convert a component and leave it mixed.
36+
37+
### ⚠️ Use Theme Tokens, Not Hard-Coded Colors
38+
39+
Never use raw color values. Access theme colors via `$themeTokens` and `$themePalette`:
40+
```vue
41+
<template>
42+
<div :style="{ color: $themeTokens.text, backgroundColor: $themeTokens.surface }">
43+
<span :style="{ color: $themeTokens.annotation }">secondary text</span>
44+
</div>
45+
</template>
46+
```
47+
For computed dynamic styles, use `$computedClass`.
48+
49+
### ⚠️ Style Blocks, Not Inline — RTL Depends On It
50+
51+
Non-dynamic styles go in `<style>` blocks. RTLCSS auto-flips directional properties (`padding-left``padding-right`) in style blocks but **cannot flip inline styles**. Dynamic directional styles must check `isRtl`.
52+
53+
### ⚠️ Prefer Composition API for New Code
54+
55+
Studio's existing code is largely Options API, but new components and refactors should use Composition API. It's fine to add Composition API to an existing Options API file when the existing logic isn't worth restructuring — be aware that a partial mix usually leads to a follow-up refactor.
56+
57+
### ⚠️ No New Vuex — Use Composition API for New State
58+
59+
Vuex 3 is deprecated in Studio. **New state goes through Composition API** — composables built on `ref`/`reactive`/`computed`, scoped to the consuming component or a `provide`/`inject` boundary. Existing Vuex modules live under `frontend/<app>/vuex/` (including the IndexedDB-backed sync store); when you're already editing code that leans on one and the slice is small, moving it to a composable is welcome — **keep it small and in-scope**, don't restructure a whole module in an unrelated PR. Leave the sync store alone unless the work is specifically about it.
60+
61+
### ⚠️ Studio Uses an IndexedDB-Backed Change-Sync Architecture — Don't Mutate Synced Models Directly
62+
63+
Edits in the editor write to Dexie tables in the browser via the Resource layer (`contentcuration/contentcuration/frontend/shared/data/`), which generates Change records that flow to the server's `/api/sync/` endpoint (`contentcuration/contentcuration/viewsets/sync/`). The server applies changes via a change-type registry (`viewsets/sync/base.py`, `viewsets/sync/constants.py`) and broadcasts back.
64+
65+
Direct `axios.post` or direct ORM `save()` on a synced model bypasses the change pipeline and corrupts the offline → online merge. **Rule:** if a model is part of the sync framework, all writes go through the Resource layer (frontend) or the change-application registry (backend). Backend system-internal operations (publishing, garbage collection) may bypass; anything user-facing must not. See `CLAUDE.md` for a fuller description.
66+
67+
### ⚠️ Responsive Layout: Plain CSS First, Vuetify Grid Only as a Last Resort
68+
69+
New layout uses plain `<div>`s with CSS (flexbox/grid) in a `<style>` block, plus KDS's `useKResponsiveWindow` composable when a breakpoint has to drive logic:
70+
```javascript
71+
import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow';
72+
73+
const { windowIsSmall, windowWidth } = useKResponsiveWindow();
74+
```
75+
Vuetify's `v-container` / `v-row` / `v-col` grid is legacy, like the rest of Vuetify. Inside a file that already uses it, leave it alone — no refactor required. Reach for it in new code only when plain CSS genuinely can't express the layout.
76+
77+
### ⚠️ Internationalize All User-Visible Text
78+
79+
Use `createTranslator` from `shared/i18n` (Studio re-exports Kolibri's i18n there) — never hard-code strings in templates:
80+
```javascript
81+
import { createTranslator } from 'shared/i18n';
82+
83+
const strings = createTranslator('ChannelStrings', {
84+
title: { message: 'Channel title', context: 'Form field label' },
85+
});
86+
const { title$ } = strings; // title$() returns the translated string
87+
```
88+
89+
### ⚠️ API Calls via the Resource Pattern
90+
91+
For synced models, use the Resource layer in `contentcuration/contentcuration/frontend/shared/data/` (see sync gotcha above). For non-synced endpoints, use the existing Resource-style wrappers in `shared/data/resources.js`. Never use raw `fetch` or `axios` for domain operations.
92+
93+
### ⚠️ Backend APIs: Use `ValuesViewset`
94+
95+
Studio has `ValuesViewset` / `ReadOnlyValuesViewset` vendored at `contentcuration/contentcuration/viewsets/base.py`. Use them for new API endpoints — define a `values` tuple and `annotate_queryset` for computed fields rather than relying on default serializer output. Apply permission classes from `contentcuration/contentcuration/viewsets/`.
96+
97+
### ⚠️ Testing Is Required
98+
99+
- **Python:** `pytest` from repo root (uses `pytest.ini``contentcuration.test_settings`). Django API tests extend `APITestCase` from `rest_framework.test`. Other Django tests extend `django.test.TestCase`.
100+
- **Frontend:** Jest runner + Vue Testing Library via `@testing-library/vue` (`render`). Write new tests with VTL. `@vue/test-utils` (`mount`/`shallowMount`) still appears in many existing tests but is **deprecated** — there are open issues to remove those tests, so do not add new ones. `describe`/`it`/`expect` are Jest globals — do NOT import them. Use `jest.fn()` and `jest.mock()`.
101+
- **TDD:** Write a failing test first, then make it pass. Especially for bug fixes — always write a test that reproduces the bug before fixing it.
102+
103+
### ⚠️ Pre-commit Auto-Fixes Files
104+
105+
When a commit fails: pre-commit auto-fixes files → **`git add` the fixed files** → re-commit. Never bypass with `--no-verify`.
106+
107+
## Project Structure
108+
109+
```
110+
studio/
111+
├── contentcuration/ # Django project root (also contains other apps)
112+
│ ├── contentcuration/ # Main Django app
113+
│ │ ├── frontend/ # Vue source
114+
│ │ │ ├── channelEdit/
115+
│ │ │ ├── channelList/
116+
│ │ │ ├── settings/
117+
│ │ │ ├── accounts/
118+
│ │ │ ├── administration/
119+
│ │ │ └── shared/ # Shared components + sync data layer
120+
│ │ ├── viewsets/ # DRF ValuesViewsets — sync/ contains the change framework
121+
│ │ ├── models.py, urls.py, settings.py, dev_settings.py, test_settings.py
122+
│ │ └── static/studio/ # webpack output (git-ignored)
123+
│ ├── automation/ # Django app — automation workflows
124+
│ ├── kolibri_content/ # Django app — Kolibri content schema
125+
│ ├── kolibri_public/ # Django app — public catalog API
126+
│ ├── search/ # Django app — full-text search
127+
│ ├── manage.py
128+
│ └── build/ # webpack-stats.json (git-ignored)
129+
├── docker/ # Dockerfile.{dev,prod,nginx.prod,postgres.dev}
130+
├── integration_testing/
131+
│ ├── features/ # Gherkin BDD specs (manual reference)
132+
│ └── smoke_test.py # CI smoke test
133+
├── jest_config/ # Jest config
134+
├── webpack.config.js
135+
├── Makefile # dc* targets + altprodserver
136+
└── docker-compose.yml / docker-compose.prod.yml
137+
```
138+
139+
**Settings split:**
140+
- `contentcuration.dev_settings` — local development. Use this for local `manage.py` commands.
141+
- `contentcuration.settings` — production. Used by `make altprodserver` and the CI smoke test.
142+
- `contentcuration.test_settings` — pytest.
143+
144+
## Code Quality
145+
146+
Studio follows the same code-quality principles as Kolibri. → See https://kolibri-dev.readthedocs.io/en/latest/code_quality.html.md for detailed examples (LLM-friendly Markdown version; drop `.md` for the HTML rendering).
147+
148+
## Key Conventions
149+
150+
**Python:** F-strings preferred. One import per line. All imports at file top — inline imports only to prevent circular imports. Descriptive migration names (no `_auto_`).
151+
152+
**Vue:** PascalCase filenames. Component `name` must match filename.
153+
154+
**Git:** Imperative commit messages. **PR titles do NOT use Conventional Commits prefix** (e.g. `feat:`, `fix:`) — plain English titles. Individual commit messages may use CC prefixes. Black/Prettier enforced by pre-commit.
155+
156+
**Don't guess — look at existing code** for patterns: `contentcuration/contentcuration/viewsets/` for API patterns, `contentcuration/contentcuration/frontend/shared/data/` for sync framework, existing `__tests__/` directories for test patterns.
157+
158+
## Running Tests
159+
160+
```bash
161+
pytest # all Python tests
162+
pytest contentcuration/contentcuration/tests/ -k name # filter by name
163+
pnpm test # all Jest tests
164+
pnpm jest --config jest_config/jest.conf.js <path> # single file
165+
pre-commit run --all-files # lint all
166+
pre-commit run --files path/to/File.vue # lint specific files
167+
```
168+
169+
Important: bare `pnpm jest` will NOT load `modulePaths` correctly — always go through `pnpm test` or pass `--config jest_config/jest.conf.js` explicitly. Always go through `pre-commit` — do not invoke ESLint / Black / Flake8 directly.
170+
171+
## Docs Reference
172+
173+
The Kolibri-dev docs site serves an LLM-friendly Markdown variant of every page — append `.md` to any URL below. The whole index is at https://kolibri-dev.readthedocs.io/en/latest/llms.txt.
174+
175+
- Code quality: https://kolibri-dev.readthedocs.io/en/latest/code_quality.html.md
176+
- Testing (general): https://kolibri-dev.readthedocs.io/en/latest/testing.html.md
177+
- Frontend testing: https://kolibri-dev.readthedocs.io/en/latest/frontend_architecture/unit_testing.html.md
178+
- Backend testing: https://kolibri-dev.readthedocs.io/en/latest/backend_architecture/testing.html.md
179+
- i18n: https://kolibri-dev.readthedocs.io/en/latest/i18n.html.md
180+
- Frontend architecture: https://kolibri-dev.readthedocs.io/en/latest/frontend_architecture/index.html.md
181+
- Backend architecture: https://kolibri-dev.readthedocs.io/en/latest/backend_architecture/index.html.md
182+
- Development workflow: https://kolibri-dev.readthedocs.io/en/latest/development_workflow.html.md
183+
184+
Local: `README.md`, `Makefile` (common commands), `docker-compose.yml` (service config).

0 commit comments

Comments
 (0)