Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
72 changes: 14 additions & 58 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,71 +88,27 @@ jobs:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}

# Set up Elixir/Phoenix server
- name: Clone Phoenix server
run: git clone --depth 1 https://github.com/replicant-sync/replicant-server.git /tmp/replicant-server

- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.17'
otp-version: '27'

- name: Cache Elixir deps
uses: actions/cache@v3
with:
path: /tmp/replicant-server/deps
key: ${{ runner.os }}-mix-${{ hashFiles('/tmp/replicant-server/mix.lock') }}

- name: Install Phoenix dependencies
working-directory: /tmp/replicant-server
run: mix deps.get

- name: Set up Phoenix database
working-directory: /tmp/replicant-server
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/replicant_server_test
SECRET_KEY_BASE: test-secret-key-base-that-is-at-least-64-characters-long-for-testing
run: |
mix ecto.create
mix ecto.migrate

- name: Generate API credentials
id: credentials
working-directory: /tmp/replicant-server
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/replicant_server_test
SECRET_KEY_BASE: test-secret-key-base-that-is-at-least-64-characters-long-for-testing
run: |
OUTPUT=$(mix replicant.gen.credentials --name "CI Test")
API_KEY=$(echo "$OUTPUT" | grep "API Key:" | awk '{print $3}')
API_SECRET=$(echo "$OUTPUT" | grep "Secret:" | awk '{print $2}')
echo "api_key=$API_KEY" >> $GITHUB_OUTPUT
echo "api_secret=$API_SECRET" >> $GITHUB_OUTPUT

- name: Start Phoenix server
working-directory: /tmp/replicant-server
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/replicant_server_test
SECRET_KEY_BASE: test-secret-key-base-that-is-at-least-64-characters-long-for-testing
run: |
mix phx.server &
for i in $(seq 1 60); do
if curl -fs http://localhost:4000/health >/dev/null; then
echo "Server is up"
exit 0
fi
sleep 1
done
echo "Server did not come up within 60s"
exit 1

- name: Run integration tests
# The harness boots the stripped server (a library: no HTTP server of its
# own) behind a minimal endpoint that mounts ReplicantServer.Sync.Socket on
# :4000, seeds an enrolled user + credential (and one legacy nil-user
# credential for the negative test), and runs the gated integration suite.
# It clones the server itself at the pinned SERVER_REF, so no separate
# checkout/credential/health steps are needed here.
- name: Boot stripped server + run integration suite
env:
RUN_INTEGRATION_TESTS: 1
REPLICANT_API_KEY: ${{ steps.credentials.outputs.api_key }}
REPLICANT_API_SECRET: ${{ steps.credentials.outputs.api_secret }}
run: cargo test --package replicant-client --test integration -- --nocapture
REPLICANT_SERVER_REF: '332a8ba' # origin/main (PR #7 merged: claim -> user_id)
REPLICANT_SERVER_DIR: /tmp/replicant-server-interop
INTEROP_DB_HOST: localhost
INTEROP_DB_USER: postgres
INTEROP_DB_PASS: postgres
INTEROP_DB_NAME: replicant_server_test
run: ./test/run_phoenix_interop_local.sh

lint:
name: Lint
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ target-docker/

# Development test scripts
test_*.sh

# Claude local session/handoff files
.claude/
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions replicant-client/examples/interactive_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
&cli.email,
&cli.api_key,
&cli.api_secret,
None,
)
.await
{
Expand Down
1 change: 1 addition & 0 deletions replicant-client/examples/simple_sync_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
&user_email,
"test-key",
"test-secret",
None,
)
.await?;

Expand Down
18 changes: 12 additions & 6 deletions replicant-client/examples/task_list_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,12 +451,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
let user_id = match db.get_user_id().await {
Ok(id) => id,
Err(_) => {
// Deterministic user ID from the identifier, or random for anonymous use
let id = if let Some(user_identifier) = &cli.user {
ClientDatabase::generate_deterministic_user_id(user_identifier)
} else {
Uuid::new_v4()
};
// Provisional random id; the server assigns a canonical id on first contact.
let id = Uuid::new_v4();

// Client ID should always be unique per client instance
let client_id = Uuid::new_v4();
Expand Down Expand Up @@ -545,6 +541,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
&user_email,
&cli.api_key,
&cli.api_secret,
None,
)
.await
{
Expand Down Expand Up @@ -630,6 +627,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
ActivityType::Error,
);
}
SyncEvent::IdentityChanged { new_user_id, .. } => {
app_state.add_activity(
format!(
"Identity adopted: {}...",
&new_user_id[..8.min(new_user_id.len())]
),
ActivityType::Connected,
);
}
}
})
{
Expand Down
4 changes: 4 additions & 0 deletions replicant-client/examples/test_rust_callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
"test-user@example.com",
"test-key",
"test-secret",
None,
)
.await
{
Expand Down Expand Up @@ -100,6 +101,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
SyncEvent::ConflictDetected { document_id, .. } => {
format!("⚠️ Conflict detected: {}", &document_id[..8])
}
SyncEvent::IdentityChanged { new_user_id, .. } => {
format!("🪪 Identity changed: {}", new_user_id)
}
};

if let Ok(mut t) = tracker_clone.lock() {
Expand Down
38 changes: 38 additions & 0 deletions replicant-client/include/replicant.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ typedef enum ReplicantEventType {
* Successfully connected to the server
*/
ConnectionSucceeded = 9,
/**
* The server-authoritative user id was adopted, replacing the local one
*/
IdentityChanged = 10,
} ReplicantEventType;

/**
Expand Down Expand Up @@ -157,6 +161,22 @@ typedef void (*ConflictEventCallback)(enum ReplicantEventType event_type,
const char *losing_content,
void *context);

/**
* Identity event callback for IdentityChanged
*
* # Parameters
* * `event_type` - Always IdentityChanged
* * `old_user_id` - The provisional/previous user id (always non-null)
* * `new_user_id` - The adopted canonical user id (always non-null)
* * `email` - The email the server resolved the id from (may be empty)
* * `context` - User-defined context pointer
*/
typedef void (*IdentityEventCallback)(enum ReplicantEventType event_type,
const char *old_user_id,
const char *new_user_id,
const char *email,
void *context);

/**
* Document structure for C API
*/
Expand Down Expand Up @@ -402,6 +422,24 @@ enum ReplicantSyncResult replicant_register_conflict_callback(struct Replicant *
ConflictEventCallback callback,
void *context);

/**
* Register a callback for IdentityChanged events
*
* # Arguments
* * `engine` - Sync engine instance
* * `callback` - Function to call when the server-authoritative id is adopted
* * `context` - User-defined context pointer passed to callback
*
* # Returns
* * SyncResult indicating success or failure
*
* # Safety
* Caller must ensure engine is valid, callback is a valid function pointer, and context pointer outlives the callback registration
*/
enum ReplicantSyncResult replicant_register_identity_callback(struct Replicant *engine,
IdentityEventCallback callback,
void *context);

/**
* Process all queued events on the current thread
*
Expand Down
1 change: 1 addition & 0 deletions replicant-client/migrations/011_add_identity_adopted.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE user_config ADD COLUMN identity_adopted INTEGER NOT NULL DEFAULT 0;
Loading
Loading