The Android build always talks to the Pi cross-origin -- a Capacitor webview serves the app from http://localhost, not from your domain -- so the browser sends a preflight OPTIONS first. It is not permitted to attach an Authorization header to that. Auth ran before anything else, so the preflight came back 401 and the real request was never attempted. There were no Access-Control-Allow-* headers either, so even a successful preflight would not have helped. Verified from an actual page before the fix: GET /api/sync and POST /api/tutor both "Failed to fetch" -- an opaque network error that points at the network rather than at middleware order. CORS now runs first and answers OPTIONS itself. Any origin is allowed by default, which is not a hole: the gate is a bearer token rather than a cookie, so a hostile page gains nothing from being allowed to send a request it cannot authenticate. HANKAN_ALLOWED_ORIGINS narrows it. backends/echo.ts is a keyless backend that reflects the request back in chunks. Deploying involves a container, a reverse proxy, a token, CORS and an SSE stream that has to survive compression -- five things that break independently, none of which involve Anthropic. HANKAN_TUTOR_BACKEND=echo proves all five from the phone before a key exists and before anything is billed. CI now runs the server that way, so the tutor endpoint is exercised over real HTTP rather than only against an injected mock. test/server/http.test.ts covers the preflight, the allow-origin header on real responses, Vary: Origin, that a bad token is still refused, and that the SSE stream parses and terminates with a done event. Verified end to end in a browser: the Pi configured through the settings panel, sync pushing 2 rows and a second sync moving 0 (the pushedAt watermark holding), the header switching from "local stand-in" to "connected", and a turn streaming back over SSE with the 8,859-character system prompt intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
2.1 KiB
YAML
72 lines
2.1 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
check:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# The curriculum gate comes first, before anything else can run.
|
|
# validate.mjs reads only data/ and lib/ and exits non-zero on a
|
|
# blocking failure. Baseline: PASS — 0 blocking, 0 advisory.
|
|
- name: Curriculum validation
|
|
run: node validate.mjs
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
cache: npm
|
|
|
|
- run: npm ci
|
|
|
|
- name: Typecheck
|
|
run: npm run typecheck
|
|
|
|
# The sync round-trip needs a real Postgres and a running server, so a
|
|
# fake would not exercise the change_seq trigger, the last-write-wins
|
|
# upsert, or cursor paging — which is where sync actually goes wrong.
|
|
- name: Start Postgres
|
|
run: |
|
|
docker run -d --name hankan-pg \
|
|
-e POSTGRES_PASSWORD=test -e POSTGRES_DB=hankan \
|
|
-p 55432:5432 postgres:16-alpine
|
|
for i in $(seq 1 30); do
|
|
docker exec hankan-pg pg_isready -U postgres -d hankan && break
|
|
sleep 1
|
|
done
|
|
|
|
- name: Start the sync server
|
|
env:
|
|
DATABASE_URL: postgres://postgres:test@localhost:55432/hankan
|
|
HANKAN_TOKEN: test-token
|
|
HANKAN_TEST_MODE: "1"
|
|
# Keyless, so the tutor endpoint is exercised over real HTTP —
|
|
# SSE framing, CORS and auth — without an Anthropic key in CI.
|
|
HANKAN_TUTOR_BACKEND: echo
|
|
PORT: "8788"
|
|
run: |
|
|
node --experimental-strip-types server/src/main.ts &
|
|
for i in $(seq 1 30); do
|
|
curl -sf http://localhost:8788/health && break
|
|
sleep 1
|
|
done
|
|
|
|
- name: Tests
|
|
env:
|
|
HANKAN_TEST_SERVER: http://localhost:8788
|
|
run: npm test
|
|
|
|
# Every word in every unit's words[] must resolve in lemma or surface.
|
|
# Runs against the committed band files in app/public/dict/.
|
|
- name: Roadmap word coverage
|
|
run: npm run dict:assert
|
|
|
|
- name: Build
|
|
run: npm run build
|