Skip to content

Conversation

sonalideshpandemsft
Copy link
Contributor

Tests that SharedString correctly raises sequenceDelta events when operations (insert, remove, annotate) are rolled back. The tests ensure that rollback emits the expected deltas and that the resulting text state matches the pre-rollback state.

This change also includes test for map and cell

AB#43938
AB#43938

@github-actions github-actions bot added area: dds Issues related to distributed data structures area: dds: sharedstring labels Aug 25, 2025
@github-actions github-actions bot added the base: main PRs targeted against main branch label Aug 25, 2025
@sonalideshpandemsft sonalideshpandemsft force-pushed the check-events-with-rollback branch from 30df064 to 41b2887 Compare August 25, 2025 18:59
@sonalideshpandemsft sonalideshpandemsft force-pushed the check-events-with-rollback branch 2 times, most recently from a513b02 to c623bdb Compare August 25, 2025 20:10
@sonalideshpandemsft sonalideshpandemsft force-pushed the check-events-with-rollback branch from c623bdb to d8fb934 Compare August 25, 2025 21:30
@sonalideshpandemsft sonalideshpandemsft marked this pull request as ready for review August 25, 2025 21:30
@Copilot Copilot AI review requested due to automatic review settings August 25, 2025 21:30
@sonalideshpandemsft sonalideshpandemsft requested a review from a team as a code owner August 25, 2025 21:30
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds comprehensive rollback testing for SharedString, SharedMap, and SharedCell DDSes. The tests validate that rollback operations correctly emit expected delta/event notifications and restore proper state when operations (insert, remove, annotate) are rolled back.

  • Refactors existing directory rollback tests to use common utilities
  • Adds new rollback test utilities (setupRollbackTest, createAdditionalClient) for multi-client scenarios
  • Implements extensive test coverage for SharedString rollback scenarios including multi-client coordination

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/dds/test-dds-utils/src/index.ts Exports new rollback test utilities
packages/dds/test-dds-utils/src/ddsTestUtils.ts New utility functions for setting up multi-client rollback tests
packages/dds/sequence/src/test/sharedString.rollback.spec.ts Comprehensive SharedString rollback tests including event validation
packages/dds/map/src/test/mocha/directory.rollback.spec.ts Refactored to use common utilities and added event validation tests
packages/dds/cell/src/test/cell.rollback.spec.ts New SharedCell rollback tests with multi-client scenarios

@sonalideshpandemsft sonalideshpandemsft force-pushed the check-events-with-rollback branch from fe59978 to 6998f8c Compare August 26, 2025 15:23
assert.equal(cell.get(), 42);

// delete triggers delete event, rollback restores valueChanged
assert.deepEqual(events, ["valueChanged", "delete", "valueChanged"]);
Copy link
Contributor

Choose a reason for hiding this comment

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

i think these tests would be easier to follow if they had more granular event validation, rather than doing it all at the end. to make that easier we could add a helper method like

function captureEvents(events: string[], emitter: {on:(e: string, l: ()=>void)=>void,off:(e: string, l: ()=>void)=>void}, act:()=>void){
	const fired: string[]=[];
	const handlers = new Map<string, ()=>void>();
	for(const event of events){
		const handler = ()=>fired.push(event)
		emitter.on(event,handler);
		handlers.set(event, handler);
	}
	try{
		act();
	}finally{
		for(const [event,handler] of handlers){
			emitter.off(event, handler)
		}
	}
	return fired;
}

this would make it easy to get the events per action, and validate per action, rather than doing it all at the end.

this would be quite a bit of refactoring. i'd probably try writing the helper method manually. migrating one test in the file, and then seeing if copilot could refactor the rest. it might save some time. be sure to commit incrementally, as copilot can also go off the rails, and you don't want to lose work.

Copy link
Contributor

Choose a reason for hiding this comment

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

this might be hard to do for the more advance tests, i think could help with the simper test

Copy link
Contributor

Choose a reason for hiding this comment

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

I light disagree with this approach, in particular with the act() callback here. Not a blocker to me either way but I'd prefer the PR's current approach.

Agree granularity is nice but is achieved more easily and blatantly obvious to a future reader by just clearing out the events array periodically.

Or if we do ultimately want a more reusable approach, the best option is probably to use an existing testing library's function mocks or spies rather than writing our own, something like toHaveBeenNthCalledWith would probably be a natural fit for this scenario. I'm less familiar with chai/sinon but I'm sure their options are fine too.

Copy link
Contributor

@ChumpChief ChumpChief left a comment

Choose a reason for hiding this comment

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

Mostly just reviewing the infra + cell + directory as I am less familiar with the sequence events.

assert.equal(cell.get(), 42);

// delete triggers delete event, rollback restores valueChanged
assert.deepEqual(events, ["valueChanged", "delete", "valueChanged"]);
Copy link
Contributor

Choose a reason for hiding this comment

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

I light disagree with this approach, in particular with the act() callback here. Not a blocker to me either way but I'd prefer the PR's current approach.

Agree granularity is nice but is achieved more easily and blatantly obvious to a future reader by just clearing out the events array periodically.

Or if we do ultimately want a more reusable approach, the best option is probably to use an existing testing library's function mocks or spies rather than writing our own, something like toHaveBeenNthCalledWith would probably be a natural fit for this scenario. I'm less familiar with chai/sinon but I'm sure their options are fine too.


const events: (string | undefined)[] = [];

cell.on("valueChanged", (value) => events.push("valueChanged"));
Copy link
Contributor

@ChumpChief ChumpChief Aug 27, 2025

Choose a reason for hiding this comment

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

These tests should also test event callback params, as these are particularly interesting in rollback cases (they need to reflect the inverse operation as well, which sometimes requires special bookkeeping in the DDS). Consider instead of just having an array of strings for the events, push some structure that also includes the args that the callback received (example with map). EDIT: I just realized cell's event params are different and maybe less interesting since they don't provide the previousValue, but still good to verify as a practice.

const dataStoreRuntime = new MockFluidDataStoreRuntime({ clientId: id });
const containerRuntime = containerRuntimeFactory.createContainerRuntime(dataStoreRuntime);

const dds = createDDS(dataStoreRuntime, id);
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems maybe a little confusing to use the same id for the clientId and the DDS? Probably not problematic but a little bit unintuitive.

Comment on lines 51 to 52
cell.set(42);
containerRuntime.flush();
Copy link
Contributor

Choose a reason for hiding this comment

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

Another approach for granularity is to shift the events off as you check them

Suggested change
cell.set(42);
containerRuntime.flush();
cell.set(42);
assert.equal(events.shift(), "valueChanged");
containerRuntime.flush();

Comment on lines +117 to +140
// Setup two clients
const {
dds: cell1,
containerRuntimeFactory,
containerRuntime: runtime1,
} = setupRollbackTest<ISharedCell>(
"client-1",
(rt, id): ISharedCell => cellFactory.create(rt, id),
);
const { dds: cell2 } = createAdditionalClient(
containerRuntimeFactory,
"client-2",
(rt, id): ISharedCell => cellFactory.create(rt, `cell-${id}`),
);

const events1: string[] = [];
const events2: string[] = [];

// Attach listeners
cell1.on("valueChanged", () => events1.push("valueChanged"));
cell1.on("delete", () => events1.push("delete"));

cell2.on("valueChanged", () => events2.push("valueChanged"));
cell2.on("delete", () => events2.push("delete"));
Copy link
Contributor

Choose a reason for hiding this comment

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

Some of this repeated setup would probably be a good candidate to move to a beforeEach block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was removed in the previous iteration as per: #25300 (comment).

Overall it seems the test is easier to follow with the initialization kept inside the test itself, even though some setup is repeated

assert.equal(cell2.get(), 42);

// Rollback delete
runtime1.rollback?.();
Copy link
Contributor

Choose a reason for hiding this comment

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

Would probably be good to do a final flush/process (that we expect to be a no-op) to verify nothing slips out that ends up in cell2/events2.

Suggested change
runtime1.rollback?.();
runtime1.rollback?.();
// After rollback, this flush/process should not affect cell2.
runtime1.flush();
containerRuntimeFactory.processAllMessages();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: dds: sharedstring area: dds Issues related to distributed data structures base: main PRs targeted against main branch
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants