Schema (idempotent migration): - payment_sessions.is_free_trial -> is_first_session_discount (data copied) - payment_sessions.mode TEXT NOT NULL DEFAULT 'chat' CHECK (chat|call) - chat_sessions.topics TEXT[] for ESP picks (info-only) New endpoints: - GET /api/client/onboarding-state (drives verif sheet + S6 paywall gate) - GET /api/client/chat-pricing (rewrite: chat+call groups + first-session discount block, per-customer eligibility) - GET /api/shared/auth-providers (env-probed; replaces ENABLE_SOCIAL_AUTH build flag — frontend cutover lands in stage 2) - GET /api/client/support-handles (Tanya Admin handles, CC-config-driven) session_warning WS event fires once at 180s remaining. app_config seeds (mock pricing tiers, first-session discount, support handles, payment method order, end-session 2-step toggle). CC SettingsPage: 3 new sections (first-session discount, pricing tiers JSON editors, support handles). 15/15 Vitest passing. chat_sessions.is_free_trial also renamed for consistency (plan only specified payment_sessions; pairing.service.js read both). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend tests (Vitest)
Vitest scaffolding for the Halo Bestie Fastify backend. Three sample tests exist to demonstrate the patterns; broader coverage will be filled in incrementally.
Strategy: schema-isolated remote DB (default)
The remote dev role on omv.sjamsani.id does not have CREATE DATABASE
privilege, so the chosen isolation mechanism is a separate schema inside the
existing halobestie_clone database. The migration runs into a halobestie_test
schema (driven by ?options=-c search_path=... on the test DB URL), leaving the
dev public schema untouched.
Valkey isolation uses a separate logical db number (/1) on the same instance.
Why not Docker?
Docker availability could not be verified inside the agent sandbox at scaffold
time. A docker-compose.test.yml exists for users who prefer ephemeral local
containers — see "Switching to local Docker" below.
Why not a separate Postgres database?
The dev role is non-superuser and lacks CREATE DATABASE. Schema isolation gives
us the same isolation guarantee (test tables live in their own namespace) without
requiring a privilege bump.
Setup
-
Copy
.env.test.example→.env.test:cp .env.test.example .env.testAdjust
TEST_DATABASE_URL/TEST_VALKEY_URLif your dev DB is elsewhere. -
(Optional) Verify connectivity:
node -e "import('postgres').then(({default:p})=>{const s=p(process.env.TEST_DATABASE_URL);s\`SELECT 1\`.then(console.log).finally(()=>s.end())})" -
The
halobestie_testschema and all test tables are created automatically the first timenpm testruns (idempotent — re-runningnpm testis safe).
Running
npm test # one-shot run
npm run test:watch # re-run on file change
npm run test:coverage # plus coverage report under coverage/
Required environment variables
| Var | Default | Purpose |
|---|---|---|
TEST_DATABASE_URL |
postgresql://halobestie_clone:halobestie_clone@omv.sjamsani.id:5432/halobestie_clone |
Same as dev — schema isolates |
TEST_DB_SCHEMA |
halobestie_test |
Schema name for test tables. Hard-rejected if set to public |
TEST_VALKEY_URL |
redis://omv.sjamsani.id:6379/1 |
Note the /1 — separate logical db from dev |
AUTH_JWT_SECRET |
(must be ≥ 32 chars) | Signs JWTs the prod authenticate plugin verifies. Test value can differ from dev |
ACCESS_TOKEN_TTL_SECONDS |
3600 |
Optional |
REFRESH_TOKEN_TTL_DAYS |
30 |
Optional |
CC_ORIGIN |
http://localhost:5173 |
Required by the internal app's CORS config |
Adding a new test
Templates by type:
| Test type | Template | Sample |
|---|---|---|
| Pure service | uses db() + fixtures |
test/services/payment.service.test.js |
| Service with mocked WS/FCM | vi.mock('../../src/plugins/websocket.js') at top |
test/services/pairing.service.test.js |
| Route (HTTP-free via inject) | app.inject({ method, url, headers, payload }) |
test/routes/client.payment.routes.test.js |
Helpers (under test/helpers/):
db.js—db()returns the shared sql client;resetDb()truncates Phase 3.7 + dependent tables;resetAppConfig()restores config defaults.valkey.js—getTestValkey()for direct keyspace assertions;flushTestDb()to wipe between tests.server.js—buildPublic()/buildInternal()for route tests.jwt.js—customerJwt(id),mitraJwt(id),ccJwt(id)mint tokens the prodauthenticateplugin accepts.authHeader(token)builds the header.fixtures.js—createCustomer(),createMitra({ isOnline }).
Patterns to follow (from the sample tests):
- Always import status / cause values from
../../src/constants.js— never hard-code'pending','all_mitras_rejected', etc. (See project memory: "Use Enums for Fixed Values".) - Mock
../../src/plugins/websocket.jsand../../src/services/notification.service.jsfor any test that touches pairing / extension / closure — they fan out via WS + FCM and you don't want either to fire on a real socket / Firebase project. - Call
resetDb()inbeforeEach,resetAppConfig()once inbeforeAll(or inafterEachif your test mutates config).
Isolation notes
Tests run sequentially (fileParallelism: false, sequence.concurrent: false)
because they share one DB schema and one Valkey db. If you ever need
parallelism: switch to per-test transactions (BEGIN in beforeEach, ROLLBACK
in afterEach) or per-test schemas (CREATE SCHEMA test_${random}) and update
vitest.config.js.
Switching to local Docker
If you'd rather run an isolated, throwaway Postgres + Valkey on your machine:
docker compose -f docker-compose.test.yml up -d
# In .env.test:
TEST_DATABASE_URL=postgresql://test:test@localhost:55432/halobestie_test
TEST_DB_SCHEMA=public
TEST_VALKEY_URL=redis://localhost:56379/0
npm test
docker compose -f docker-compose.test.yml down -v
The non-default ports (55432, 56379) avoid clashing with any local Postgres /
Redis you have running. Note TEST_DB_SCHEMA=public is OK in the Docker case
because the whole database is throwaway — schema isolation is only required
when sharing with the dev DB.
Safety guards
setup.jshard-fails ifTEST_DB_SCHEMA === 'public'ANDTEST_DATABASE_URLlooks like the dev DB. (Schema reuse on the dev DB would clobber dev tables.)setup.jshard-fails if any required env var is missing — silent fallback to dev URLs would be catastrophic.- The migration runs as a child process (not in-process) so its
sql.end()at the bottom doesn't tear down the singleton this test process shares with services.