Skip to content

Conversation

@chris-bes
Copy link
Contributor

Changes:

  • Sync tests

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @chris-bes, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the synchronization capabilities by introducing a dedicated SyncLookup model for better data tracking and refining the SyncSession management with improved state transitions and error handling. A major focus has been placed on bolstering test coverage for the CentralSyncManager, ensuring robustness across pull, push, and session operations, and introducing new test utilities to facilitate more complex testing scenarios. Configuration adjustments, such as enabling avoidRepull by default, aim to optimize sync performance.

Highlights

  • New SyncLookup Model: Introduced a new SyncLookup model and its corresponding record to manage synchronization lookup data within the database, enhancing the tracking of sync-related information.
  • Enhanced SyncSession Management: The SyncSessionModel now includes a markAsStartedAt method, allowing for more precise tracking of when a sync session officially begins. Error handling for sync sessions has also been refined with a new helper function errorMessageFromSessionId.
  • Comprehensive Sync Test Coverage: Added extensive test files for CentralSyncManager covering critical synchronization functionalities such as pull operations (including concurrent transactions), push operations, session lifecycle management (start, connect, end), and SyncLookup table updates. An old example test file was removed.
  • Test Utility Improvements: New test utilities like setupTestApp, waitForSession, and waitForPushComplete have been added to streamline testing of sync processes. A pauseSnapshotProcess hook was introduced in snapshotOutgoingChanges specifically for testing concurrent snapshot scenarios.
  • Configuration Update: The default configuration for CentralSyncManager now sets avoidRepull to true, which can optimize sync performance by preventing unnecessary re-pulls of data for devices.
  • Test Script Refinement: A new package:test:serial script was added to package.json for running Jest tests in serial mode, and the packages/sync-server's test script was updated to utilize this for more controlled test execution.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive suite of tests for the sync functionality, significantly improving the robustness of the CentralSyncManager. The new tests cover pull, push, session management, and the sync lookup table logic, which is a valuable addition. The changes also include minor refactorings and additions to support this testing effort, such as a test hook for pausing snapshots and enabling the avoidRepull feature by default. My review focuses on enhancing type safety within the new test files by replacing the use of any with more specific types, which will improve maintainability and code clarity.

import { waitForPushComplete, waitForSession } from '../testUtilities/waitForSync';

describe('CentralSyncManager.push', () => {
let models: any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve type safety, please avoid using any. You can type models as TestSyncServerModelRegistry by importing it from ../types.

Suggested change
let models: any;
let models: TestSyncServerModelRegistry;

Copy link
Contributor

@jaskfla jaskfla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Buncha small optional details, but nothing to complain about. Looking great overall—happy for you to pick and choose which comments need addressing and merge without re-review ✅

Thanks for sorting this out 🙏

* @param {number} tick
*/
async markAsStartedAt(id, tick) {
await this.updateById(id, { started_at_tick: tick });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small stylistic thing, but could be nice to be explicit that we’re throwing away the return value

Suggested change
await this.updateById(id, { started_at_tick: tick });
void (await this.updateById(id, { started_at_tick: tick }));

"start-verbose": "LOG_LEVEL=debug yarn run start-dev",
"setup-test": "yarn workspace @tupaia/database run setup-test-database",
"test": "yarn run package:test"
"test": "yarn run package:test:serial"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only usage of :serial, right? Maybe clearer just to call jest --runInBand here?

Copy link
Contributor

@jaskfla jaskfla Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve had a quick squiz and it seems like the --maxWorkers=50% we set in the global script is Jest’s default anyway

Simpler to just remove --maxWorkers=50% from the global script. Here, call yarn run package:test --runInBand and let Yarn forward the argument

Comment on lines 21 to 24
const POLICY = {
DL: [BES_ADMIN_PERMISSION_GROUP, 'Admin'],
KI: [TUPAIA_ADMIN_PANEL_PERMISSION_GROUP, 'Admin'],
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const POLICY = {
DL: [BES_ADMIN_PERMISSION_GROUP, 'Admin'],
KI: [TUPAIA_ADMIN_PANEL_PERMISSION_GROUP, 'Admin'],
};
const POLICY = {
DL: [BES_ADMIN_PERMISSION_GROUP, 'Admin'],
KI: [TUPAIA_ADMIN_PANEL_PERMISSION_GROUP, 'Admin'],
} as const;

Ditto with SYNC_CONFIG in CentralSyncManager.syncLookup.test.ts

);
});

it("excludes inserted records from another sync session when the current' session's snapshot transaction already started", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it("excludes inserted records from another sync session when the current' session's snapshot transaction already started", async () => {
it('excludes inserted records from another sync session when the current sessions snapshot transaction already started', async () => {

recordId: userAccount4.id,
data: await userAccount4.getData(),
},
] as SyncSnapshotAttributes[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly dangerous cast? Looks like the array elements are missing a couple properties that SyncSnapshotAttributes requires

Happy to overlook this in test environment, though—your call 🙂


describe('startSession', () => {
it('creates a new session', async () => {
const centralSyncManager = new CentralSyncManager(models);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: could declare let centralSyncManager: CentralSyncManager in a higher scope and do centralSyncManager = new CentralSyncManager(models) in the beforeEach phase

Comment on lines +221 to +223
record_id: expect.anything(),
record_type: model.databaseRecord,
project_ids: expect.anything(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth being stricter here?

Suggested change
record_id: expect.anything(),
record_type: model.databaseRecord,
project_ids: expect.anything(),
record_id: expect.stringMatcing(/^[a-f\d]{24}$/),
record_type: model.databaseRecord,
project_ids: expect.arrayOf(expect.stringMatching(/^[a-f\d]{24}$/));

expect.any(String) also works as a middle ground, but matching the pattern feels right there

(.anything() appears a few times in this test suite, but I won’t repeat this comment)

lookupTable: {
perModelUpdateTimeoutMs: 1_000_000,
avoidRepull: false,
avoidRepull: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double checking this was an intentional change?

// @ts-expect-error - central-server createApp is a JS file without types
import { createApp } from '../../../central-server/src/createApp';

const models = getTestModels() as ModelRegistry & { user: any };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const models = getTestModels() as ModelRegistry & { user: any };
const models = getTestModels() as SyncServerModelRegistry;

(Remember to update imports)

Comment on lines +5 to +9
let ready = false;
while (!ready) {
ready = await centralSyncManager.checkSessionReady(sessionId);
await sleep(100);
}
Copy link
Contributor

@jaskfla jaskfla Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of those rare instances where dowhile makes sense!

Honestly not fussed about changing; I like the readability of the ready variable. This is mostly just a reminder that do loops exist in JS 😆

Suggested change
let ready = false;
while (!ready) {
ready = await centralSyncManager.checkSessionReady(sessionId);
await sleep(100);
}
do {
await sleep(100);
} while (!(await centralSyncManager.checkSessionReady(sessionId)))

@jaskfla jaskfla force-pushed the rn-1717-test-four branch from 55e4843 to 2600161 Compare January 5, 2026 22:55
record_type: 'user_account',
updated_at_sync_tick: expectedTick.toString(),
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test assertion never validates record data

The forEach callback doesn't receive any parameters (uses () instead of (record)), and expect.objectContaining({...}) by itself creates a matcher but never actually asserts anything. The matcher needs to be used with expect(record).toEqual(...) or similar. This test will always pass regardless of the actual data in userAccountLookupData, effectively making it useless for verifying the updated_at_sync_tick values.

Fix in Cursor Fix in Web


await centralSyncManager.connectToSession(sessionId);

expect(() => centralSyncManager.connectToSession(sessionId)).not.toThrow();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test uses synchronous assertion on async function

The test uses expect(() => connectToSession(...)).not.toThrow() on an async function, but .toThrow() only catches synchronous throws, not Promise rejections. Since connectToSession is async, this assertion will always pass even if the function returns a rejected Promise. The correct pattern is await expect(...).resolves.toBeDefined() as used elsewhere (e.g., line 189 uses the async-aware .rejects.toThrow()).

Fix in Cursor Fix in Web

lookupTable: {
perModelUpdateTimeoutMs: 1_000_000,
avoidRepull: false,
avoidRepull: true,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default config change in test-focused PR

The default value for avoidRepull in DEFAULT_CONFIG was changed from false to true. This changes production behavior in a PR that's titled as "Sync tests" and should only be adding tests. A reviewer in the discussion explicitly asked "Double checking this was an intentional change?" suggesting this may have been accidentally included. This behavioral change affects how sync data is handled for devices and could impact production sync behavior if not intentional.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants