Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f1ceacf
fix: parse JSON response from cloudsync_network_sync
damlayildiz Mar 3, 2026
38a6fad
chore: add test-suite plan
damlayildiz Mar 6, 2026
b9816db
chore: add @testing-library/react-native for hook tests
damlayildiz Mar 6, 2026
4091751
test: add shared mocks for native modules
damlayildiz Mar 6, 2026
7f57f07
test: add pure function and createDatabase tests (46 tests)
damlayildiz Mar 6, 2026
0f8638b
test: add core logic tests (67 tests)
damlayildiz Mar 6, 2026
cfe6d90
test: add pushNotificationSyncTask, optionalDependencies, and context…
damlayildiz Mar 6, 2026
99eaf44
test: add hook tests for useSqliteExecute, useSqliteTransaction, useO…
damlayildiz Mar 6, 2026
524f7d7
test: add complex hook tests for sync manager, lifecycle, polling, pu…
damlayildiz Mar 6, 2026
f78808d
test: add useDatabaseInitialization and SQLiteSyncProvider integratio…
damlayildiz Mar 6, 2026
6e7cec3
test: add coverage for permission fallback, auth, and config in SQLit…
damlayildiz Mar 6, 2026
adcd0a3
chore: update gitignore
damlayildiz Mar 6, 2026
387c95d
fix: skip push token registration when siteId is unavailable
damlayildiz Mar 6, 2026
29ec55b
test: add Tier 1 coverage gap tests (262 total, 82.96% branches)
damlayildiz Mar 9, 2026
34f43b5
test: add Tier 2 coverage gap tests (272 total, 85.47% branches)
damlayildiz Mar 9, 2026
7c87fdb
test: suppress console.warn leak in backgroundSyncRegistry test
damlayildiz Mar 9, 2026
38491da
chore: update .claude/plans/test-suite-design.md
damlayildiz Mar 9, 2026
39f2073
fix: correct push token registration auth header and base URL
damlayildiz Mar 9, 2026
09e341f
fix: resolve TypeScript errors across test files
damlayildiz Mar 9, 2026
6847368
fix: imports
damlayildiz Mar 9, 2026
9ba24e8
fix: update network_init params
damlayildiz Mar 10, 2026
efd1192
fix: initializeSyncExtension
damlayildiz Mar 10, 2026
3e6e1d1
fix: push fallback behavior
damlayildiz Mar 10, 2026
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
111 changes: 111 additions & 0 deletions .claude/plans/test-suite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Test Suite

**Status:** Implemented
**Last updated:** 2026-03-10
**Total:** 31 test files, 272 tests

## How to Run

```bash
# Run all tests
yarn test

# Run with coverage report
yarn test --coverage

# Open HTML coverage report
open coverage/lcov-report/index.html
```

## Stack

- **Runner:** Jest (react-native preset)
- **Hooks:** `renderHook` from `@testing-library/react-native`
- **Mocks:** Co-located `__mocks__/` directories for native modules

## Architecture

Tests are co-located next to source files in `__tests__/` directories, organized in 5 layers:

1. **Pure functions** — no mocks needed
2. **Core logic** — mocked native modules (op-sqlite, NetInfo, Expo)
3. **Context consumer hooks** — wrapped in test providers
4. **Complex hooks** — renderHook with mocked dependencies
5. **Integration** — SQLiteSyncProvider rendering

## Shared Mocks

Located in `src/__mocks__/`:

| Mock | What it provides |
|------|-----------------|
| `@op-engineering/op-sqlite` | `createMockDB()`, `open()`, `getDylibPath()` |
| `@react-native-community/netinfo` | `addEventListener`, `fetch`, `__emit()` |
| `react-native` | `AppState`, `Platform` |
| `expo-notifications` | Token, permissions, listeners |
| `expo-secure-store` | `getItemAsync`, `setItemAsync`, `deleteItemAsync` |
| `expo-task-manager` | `defineTask`, `isTaskRegisteredAsync` |
| `expo-constants` | `expoConfig.extra` |
| `expo-application` | `getIosIdForVendorAsync`, `getAndroidId` |

Test utilities in `src/testUtils.tsx` provide `createTestWrapper` for provider-wrapped hook tests.

## Test Files (31 files, 272 tests)

### Layer 1: Pure Functions (39 tests)

| Test file | Source | Tests | What's tested |
|-----------|--------|------:|---------------|
| `core/polling/__tests__/calculateAdaptiveSyncInterval.test.ts` | calculateAdaptiveSyncInterval | 10 | Base interval, idle backoff at threshold, exponential error backoff, caps at maxInterval, error priority over idle |
| `core/pushNotifications/__tests__/isSqliteCloudNotification.test.ts` | isSqliteCloudNotification | 13 | Foreground valid/invalid URI, iOS background body, Android JSON string body, Android dataString fallback, invalid JSON, wrong URI, empty data |
| `core/common/__tests__/logger.test.ts` | logger | 9 | debug=true logs info/warn, debug=false suppresses, error always logs, [SQLiteSync] prefix, ISO timestamp, default debug=false |
| `core/pushNotifications/__tests__/pushNotificationSyncCallbacks.test.ts` | pushNotificationSyncCallbacks | 5 | Register/get background callback, null default, set/get/clear foreground callback |
| `core/__tests__/constants.test.ts` | constants | 2 | FOREGROUND_DEBOUNCE_MS value, BACKGROUND_SYNC_TASK_NAME non-empty |

### Layer 2: Core Logic (97 tests)

| Test file | Source | Tests | What's tested |
|-----------|--------|------:|---------------|
| `core/database/__tests__/createDatabase.test.ts` | createDatabase | 9 | Opens DB, WAL journal mode, write mode pragmas, read mode pragmas, returns DB, propagates open/pragma errors |
| `core/sync/__tests__/initializeSyncExtension.test.ts` | initializeSyncExtension | 14 | Missing connectionString/auth validation, iOS/Android extension paths, version check, cloudsync_init per table, network_init, API key/accessToken auth, siteId logging |
| `core/sync/__tests__/executeSync.test.ts` | executeSync | 14 | JS retry loop (returns 0/count, stops on changes, max attempts, transaction wrapping, malformed JSON), native retry passthrough |
| `core/background/__tests__/backgroundSyncConfig.test.ts` | backgroundSyncConfig | 10 | Persist/get/clear config, null without SecureStore, parse errors, warn/error handling |
| `core/background/__tests__/backgroundSyncRegistry.test.ts` | backgroundSyncRegistry | 7 | Register (persist + task), unregister (task + clear), warns when unavailable, error handling |
| `core/background/__tests__/executeBackgroundSync.test.ts` | executeBackgroundSync | 13 | Opens DB, inits sync, executes with native retry, updateHook callback, changes collection, DB close in finally, error rethrow, close error handling |
| `core/pushNotifications/__tests__/registerPushToken.test.ts` | registerPushToken | 12 | Skip duplicate, correct URL, accessToken/apiKey auth headers, body fields, iOS/Android device ID, non-ok response, persist after success, SecureStore read/write errors, missing expo-application |
| `core/pushNotifications/__tests__/pushNotificationSyncTask.test.ts` | pushNotificationSyncTask | 8 | Task definition with/without ExpoTaskManager, handler routes to background sync, skips non-SQLite notification, foreground callback when app active, error handling, skip without config |
| `core/common/__tests__/optionalDependencies.test.ts` | optionalDependencies | 10 | Each Expo module available/null, ExpoConstants .default fallback, isBackgroundSyncAvailable (all present vs any missing) |

### Layer 3: Context Consumer Hooks (10 tests)

| Test file | Source | Tests | What's tested |
|-----------|--------|------:|---------------|
| `hooks/context/__tests__/useSqliteDb.test.ts` | useSqliteDb | 2 | Returns writeDb/readDb/initError from context, null defaults |
| `hooks/context/__tests__/useSyncStatus.test.ts` | useSyncStatus | 2 | Returns all status fields, default values |
| `hooks/context/__tests__/useSqliteSync.test.ts` | useSqliteSync | 2 | Returns merged contexts, triggerSync callable |
| `core/common/__tests__/useInternalLogger.test.ts` | useInternalLogger | 2 | Returns logger from context, has info/warn/error methods |
| `hooks/sync/__tests__/useTriggerSqliteSync.test.ts` | useTriggerSqliteSync | 2 | Returns triggerSync, calls through to context |

### Layer 4: Complex Hooks (112 tests)

_Includes useDatabaseInitialization which spans init + lifecycle._

| Test file | Source | Tests | What's tested |
|-----------|--------|------:|---------------|
| `hooks/sqlite/__tests__/useSqliteExecute.test.ts` | useSqliteExecute | 10 | Undefined when no db, execute on writeDb/readDb, error state, clears error, auto-sync after write, skip on readOnly/autoSync=false, non-Error wrapping |
| `hooks/sqlite/__tests__/useSqliteTransaction.test.ts` | useSqliteTransaction | 8 | Undefined when no writeDb, calls transaction, error state, clears error, auto-sync after commit, skip autoSync=false, non-Error wrapping |
| `hooks/sqlite/__tests__/useOnTableUpdate.test.ts` | useOnTableUpdate | 8 | Register/remove updateHook, table filtering, null row for DELETE, empty rows, fetch error, no-op when null |
| `hooks/sync/__tests__/useSqliteSyncQuery.test.ts` | useSqliteSyncQuery | 10 | Loading state, initial read, error, reactive subscription after debounce, callback updates, unmount cleanup, debounce clearing, stale subscription skip, unsubscribe |
| `core/sync/__tests__/useSyncManager.test.ts` | useSyncManager | 16 | Null db/not ready guards, sync lifecycle, empty sync counters, error state, interval recalculation (polling vs push), concurrent sync prevention, Android network check, error backoff |
| `core/sync/__tests__/useInitialSync.test.ts` | useInitialSync | 4 | Delayed trigger after 1500ms, not ready guard, once-only, cleanup on unmount |
| `core/lifecycle/__tests__/useAppLifecycle.test.ts` | useAppLifecycle | 10 | Register/remove AppState listener, foreground sync trigger, interval reset (polling only), debounce, background state tracking |
| `core/lifecycle/__tests__/useNetworkListener.test.ts` | useNetworkListener | 10 | Register/unsubscribe NetInfo, sync on reconnect, no sync online→online, background guard, isNetworkAvailable state, null isInternetReachable/isConnected handling |
| `core/polling/__tests__/useAdaptivePollingSync.test.ts` | useAdaptivePollingSync | 9 | Start/stop polling, no start in push/not ready, pause on background, resume, dynamic interval, cleanup |
| `core/pushNotifications/__tests__/usePushNotificationSync.test.ts` | usePushNotificationSync | 15 | Permission request, skip in polling, token registration, siteId retrieval, denied callback, foreground listener, sync trigger, ignore non-SQLite notification, background registration, fallback, unregister on mode switch, cleanup, handle failures |
| `core/database/__tests__/useDatabaseInitialization.test.ts` | useDatabaseInitialization | 12 | Creates write/read DBs, initializes sync extension, onDatabaseReady callback, re-init on config change, error handling, empty name/tables validation, close errors on unmount |

### Layer 5: Integration (14 tests)

| Test file | Source | Tests | What's tested |
|-----------|--------|------:|---------------|
| `core/__tests__/SQLiteSyncProvider.test.tsx` | SQLiteSyncProvider | 14 | Renders children, provides writeDb/readDb, initError/syncError, onDatabaseReady, default status, syncMode, triggerSync, adaptive config, re-init triggers, mode fallback, cleanup |
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,8 @@ ios/
# React Native Nitro Modules
nitrogen/

# Jest coverage
coverage/

# Example app environment files
examples/*/.env
105 changes: 105 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,26 @@ Every exported function, interface, and type gets JSDoc documentation:
export function myFunction(foo: string): number { ... }
```

### JSDoc for types and interfaces
All interfaces and types get a JSDoc comment, and every field gets an inline `/** comment */`:
```typescript
/** Props for {@link MyComponent} */
interface MyComponentProps {
/** Whether the dialog is visible */
open: boolean;
/** Called to close the dialog */
onClose: () => void;
}

/** Single navigation link in the sidebar */
type NavItem = {
/** Display text */
label: string;
/** Route path */
href: string;
};
```

### Section markers inside functions
Use `/** SECTION NAME */` to mark logical sections within complex functions:
```typescript
Expand Down Expand Up @@ -106,3 +126,88 @@ if (Platform.OS === 'android') { ... }
- Obvious code (`// increment counter` before `count++`)
- Code that's already clear from good naming
- Every single line - only where it adds value

## 6. Learn From Mistakes

**When corrected, codify the lesson. Don't repeat the same mistake twice.**

When the user corrects you or you discover a pattern/convention mid-session:
1. Acknowledge the mistake explicitly.
2. Propose a new rule or update to an existing section of this file that would have prevented it.
3. After user approval, add/update the rule in CLAUDE.md so future sessions benefit.

Examples of things worth capturing:
- File/folder conventions the user enforces (e.g. "utils go under `src/lib/utils/`")
- Naming patterns (e.g. "one function per file, kebab-case filename")
- Architectural preferences revealed through feedback
- Anti-patterns the user flags

Do NOT add rules speculatively. Only add rules that come from actual corrections or explicit user preferences expressed during a session.

## 7. Workflow Orchestration

### Plan Mode Default

- Enter plan mode for ANY non-trivial task (3+ steps or architectural decisions)
- If something goes sideways, STOP and re-plan immediately — don't keep pushing
- Use plan mode for verification steps, not just building
- Write detailed specs upfront to reduce ambiguity

### Subagent Strategy

- Use subagents liberally to keep main context window clean
- Offload research, exploration, and parallel analysis to subagents
- For complex problems, throw more compute at it via subagents
- One task per subagent for focused execution

### Self-Improvement Loop

- After ANY correction from the user: update relevant files in `.claude/rules/` with the pattern
- Write rules for yourself that prevent the same mistake
- Ruthlessly iterate on these lessons until mistake rate drops
- Review lessons at session start for relevant project

### Verification Before Done

- Never mark a task complete without proving it works
- Diff behavior between main and your changes when relevant
- Ask yourself: "Would a staff engineer approve this?"
- Run tests, check logs, demonstrate correctness

### Demand Elegance (Balanced)

- For non-trivial changes: pause and ask "is there a more elegant way?"
- If a fix feels hacky: "Knowing everything I know now, implement the elegant solution"
- Skip this for simple, obvious fixes — don't over-engineer
- Challenge your own work before presenting it

### Autonomous Bug Fixing

- When given a bug report: just fix it. Don't ask for hand-holding
- Point at logs, errors, failing tests — then resolve them
- Zero context switching required from the user
- Go fix failing CI tests without being told how

## 8. Plan Files

**Store implementation plans in `.claude/plans/<feature-name>.md`**

- Plans live in `.claude/plans/` so Claude can reference them across sessions
- Use descriptive kebab-case names: `test-suite-design.md`, `push-notification-refactor.md`
- Include date and approval status at the top
- These are working documents — update them as the plan evolves

## 9. Task Management

1. **Plan First**: Write plan to `.claude/todo/<feature-or-task-name>.md` with checkable items
2. **Verify Plan**: Check in before starting implementation
3. **Track Progress**: Mark items complete as you go
4. **Explain Changes**: High-level summary at each step
5. **Document Results**: Add review section to `.claude/todo/<feature-or-task-name>.md`
6. **Capture Lessons**: Update relevant files in `.claude/rules/` after corrections

## 10. Core Principles

- **Simplicity First**: Make every change as simple as possible. Impact minimal code.
- **No Laziness**: Find root causes. No temporary fixes. Senior developer standards.
- **Minimal Impact**: Changes should only touch what's necessary. Avoid introducing bugs.
39 changes: 33 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ npx expo run:android
Configure OffSync by following the [OffSync setup guide](https://docs.sqlitecloud.io/docs/offsync#:~:text=in%20the%20cloud.-,Configuring%20OffSync,-You%20can%20enable).

4. **Get credentials**
Copy your **connection string** and **API key** from the dashboard.
Copy your **project ID** and **API key** from the dashboard.
Set `organizationID` to your sqlite-sync provider ID. For SQLite Cloud, use `org_sqlitecloud`.
- Alternatively, you can use [access tokens](https://docs.sqlitecloud.io/docs/access-tokens) for [Row-Level Security](https://docs.sqlitecloud.io/docs/rls).

### 2. Wrap Your App
Expand All @@ -138,7 +139,8 @@ import { SQLiteSyncProvider } from '@sqliteai/sqlite-sync-react-native';
export default function App() {
return (
<SQLiteSyncProvider
connectionString="sqlitecloud://your-host.sqlite.cloud:8860/your-database"
projectID="your-project-id"
organizationID="org_sqlitecloud"
databaseName="myapp.db"
apiKey="your-api-key"
syncMode="polling"
Expand Down Expand Up @@ -327,7 +329,8 @@ Main provider component that enables sync functionality.

| Prop | Type | Required | Description |
| ------------------------------- | --------------------------- | -------- | ---------------------------------------------------------- |
| `connectionString` | `string` | ✅ | SQLite Cloud connection string |
| `projectID` | `string` | ✅ | SQLite Cloud project ID |
| `organizationID` | `string` | ✅ | Sync provider organization ID (`org_sqlitecloud` for SQLite Cloud) |
| `databaseName` | `string` | ✅ | Local database file name |
| `tablesToBeSynced` | `TableConfig[]` | ✅ | Array of tables to sync |
| `apiKey` | `string` | \* | API key for authentication |
Expand Down Expand Up @@ -365,12 +368,22 @@ Uses push notifications from SQLite Cloud:
```typescript
// Polling mode with default settings (recommended)
<SQLiteSyncProvider
projectID="your-project-id"
organizationID="org_sqlitecloud"
databaseName="myapp.db"
apiKey="your-api-key"
tablesToBeSynced={[...]}
syncMode="polling"
// Uses defaults: baseInterval=5s, maxInterval=5min, emptyThreshold=5
>

// Polling mode with custom intervals
<SQLiteSyncProvider
projectID="your-project-id"
organizationID="org_sqlitecloud"
databaseName="myapp.db"
apiKey="your-api-key"
tablesToBeSynced={[...]}
syncMode="polling"
adaptivePolling={{
baseInterval: 3000, // 3s base interval
Expand All @@ -381,6 +394,11 @@ Uses push notifications from SQLite Cloud:

// Push mode (requires expo-notifications)
<SQLiteSyncProvider
projectID="your-project-id"
organizationID="org_sqlitecloud"
databaseName="myapp.db"
apiKey="your-api-key"
tablesToBeSynced={[...]}
syncMode="push"
// Automatically falls back to polling if permissions denied
>
Expand Down Expand Up @@ -425,7 +443,8 @@ Use `onDatabaseReady` to run migrations or other setup after the database opens

```typescript
<SQLiteSyncProvider
connectionString="..."
projectID="..."
organizationID="..."
databaseName="myapp.db"
apiKey="..."
tablesToBeSynced={[...]}
Expand All @@ -449,6 +468,11 @@ When using push mode, the system will prompt the user for notification permissio

```tsx
<SQLiteSyncProvider
projectID="your-project-id"
organizationID="org_sqlitecloud"
databaseName="myapp.db"
apiKey="your-api-key"
tablesToBeSynced={[...]}
syncMode="push"
renderPushPermissionPrompt={({ allow, deny }) => (
<Modal visible animationType="fade" transparent>
Expand All @@ -463,7 +487,6 @@ When using push mode, the system will prompt the user for notification permissio
</View>
</Modal>
)}
// ...other props
>
<YourApp />
</SQLiteSyncProvider>
Expand Down Expand Up @@ -1144,7 +1167,11 @@ Enable detailed logging during development:

```typescript
<SQLiteSyncProvider
// ... other props
projectID="your-project-id"
organizationID="org_sqlitecloud"
databaseName="myapp.db"
apiKey="your-api-key"
tablesToBeSynced={[...]}
debug={__DEV__} // Enable in development only
>
```
Expand Down
13 changes: 8 additions & 5 deletions examples/sync-demo-bare/.env.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# SQLite Cloud Configuration
# Copy this file to .env and fill in your actual values

# Your SQLite Cloud connection string
# Your SQLite Cloud project ID
# Get this from: https://dashboard.sqlitecloud.io/ > Your Project > Configuration
# Format: sqlitecloud://your-host.sqlite.cloud:8860/your-database-name
SQLITE_CLOUD_CONNECTION_STRING=
SQLITE_CLOUD_PROJECT_ID=

# Sync provider organization ID
# For SQLite Cloud, use: org_sqlitecloud
SQLITE_CLOUD_ORGANIZATION_ID=

# Your SQLite Cloud API Key
# Get this from: https://dashboard.sqlitecloud.io/ > Your Project > Configuration
SQLITE_CLOUD_API_KEY=

# Local database name (can be any name you prefer)
DATABASE_NAME=sync-demo.db
DATABASE_NAME=

# Table name to sync (must match the table created in SQLite Cloud)
TABLE_NAME=test_table
TABLE_NAME=
3 changes: 2 additions & 1 deletion examples/sync-demo-bare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ Follow the [React Native environment setup guide](https://reactnative.dev/docs/s
2. **Fill in your credentials** in the `.env` file:

```env
SQLITE_CLOUD_CONNECTION_STRING=sqlitecloud://your-host.sqlite.cloud:8860/your-database
SQLITE_CLOUD_PROJECT_ID=your-project-id
SQLITE_CLOUD_ORGANIZATION_ID=your-organization-id
SQLITE_CLOUD_API_KEY=your-api-key-here
DATABASE_NAME=sync-demo.db
TABLE_NAME=test_table
Expand Down
Loading