Skip to content
147 changes: 147 additions & 0 deletions packages/dds/map/src/test/directoryOracle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { strict as assert } from "node:assert";

import type { IEventThisPlaceHolder } from "@fluidframework/core-interfaces";

import type {
IDirectory,
IDirectoryValueChanged,
ISharedDirectory,
IValueChanged,
} from "../interfaces.js";

/**
* Oracle for directory
* @internal
*/
export class SharedDirectoryOracle {
private readonly model = new Map<string, unknown>();

public constructor(private readonly sharedDir: ISharedDirectory) {
this.sharedDir.on("valueChanged", this.onValueChanged);
this.sharedDir.on("clear", this.onClear);
this.sharedDir.on("subDirectoryCreated", this.onSubDirCreated);
this.sharedDir.on("subDirectoryDeleted", this.onSubDirDeleted);
this.sharedDir.on("containedValueChanged", this.onContainedValueChanged);

this.takeSnapshot(sharedDir);
}

private takeSnapshot(dir: ISharedDirectory | IDirectory): void {
for (const [k, v] of this.sharedDir.entries()) {
this.model.set(k, v);
}
}

private readonly onValueChanged = (
changed: IDirectoryValueChanged,
local: boolean,
target: IEventThisPlaceHolder,
): void => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { path, key, previousValue } = changed;
const fullPath = path === "/" ? `/${key}` : `${path}/${key}`;

if (this.model.has(fullPath)) {
const prevVal = this.model.get(fullPath);
assert.strictEqual(
prevVal,
previousValue,
`previous value mismatch at ${fullPath}: expected: ${prevVal}, actual: ${previousValue}`,
);
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const newVal = this.sharedDir.get(fullPath);
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems wrong. i don't think you can call get a full path like this, i think you need to find the directory with getWorkingDirectory, and then get the specific key off that sub-directory


if (newVal === undefined) {
// deletion
this.model.delete(fullPath);
} else {
this.model.set(fullPath, newVal);
}
};

private readonly onClear = (local: boolean, target: IEventThisPlaceHolder): void => {
this.model.clear();
};

private readonly onSubDirCreated = (
path: string,
local: boolean,
target: IEventThisPlaceHolder,
): void => {
this.model.set(path, undefined);
};

private readonly onSubDirDeleted = (
path: string,
local: boolean,
target: IEventThisPlaceHolder,
): void => {
this.model.delete(path);
};

private readonly onContainedValueChanged = (
changed: IValueChanged,
local: boolean,
target: IEventThisPlaceHolder,
): void => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { key, previousValue } = changed;

if (this.model.has(key)) {
const prevVal = this.model.get(key);
assert.strictEqual(
prevVal,
previousValue,
`contained previous value mismatch at ${key}: expected: ${prevVal}, actual: ${previousValue}`,
);
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const newVal = this.sharedDir.get(key);
if (newVal === undefined) {
this.model.delete(key);
} else {
this.model.set(key, newVal);
}
};

public validate(): void {
for (const [pathKey, value] of this.model.entries()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

i'm not sure this is correct. i think entries just returns the keys of the directory and you have to use subdirectories to get the subdirectories

Copy link
Contributor Author

@sonalideshpandemsft sonalideshpandemsft Sep 29, 2025

Choose a reason for hiding this comment

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

model map stores subdirectories as separate keys, so each subdirectory has its own entry

nvm - subdirs are not pushed to the snapshot. looking into it...

const parts = pathKey.split("/").filter((p) => p.length > 0);
assert(parts.length > 0, "Invalid path, cannot extract key");
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

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

Use a string literal for the assert error message instead of a descriptive string

Copilot generated this review using guidance from repository custom instructions.

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const leafKey = parts.pop()!; // The actual key
let dir: IDirectory | undefined = this.sharedDir;

for (const part of parts) {
dir = dir.getSubDirectory(part);
if (!dir) break;
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const actual = dir?.get(leafKey);
assert.deepStrictEqual(
actual,
value,
`SharedDirectoryOracle mismatch at path="${pathKey}" with actual value = ${actual} and oracle value = ${value} with model entries = ${JSON.stringify(this.model.entries())}}`,
);
}
}

public dispose(): void {
this.sharedDir.off("valueChanged", this.onValueChanged);
this.sharedDir.off("clear", this.onClear);
this.sharedDir.off("subDirectoryCreated", this.onSubDirCreated);
this.sharedDir.off("subDirectoryDeleted", this.onSubDirDeleted);
this.sharedDir.off("containedValueChanged", this.onContainedValueChanged);
this.model.clear();
}
}
34 changes: 32 additions & 2 deletions packages/dds/map/src/test/mocha/directoryFuzzTests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@

import * as dirPath from "node:path";

import { TypedEventEmitter } from "@fluid-internal/client-utils";
import { takeAsync } from "@fluid-private/stochastic-test-utils";
import { type DDSFuzzModel, createDDSFuzzSuite } from "@fluid-private/test-dds-utils";
import {
type DDSFuzzHarnessEvents,
type DDSFuzzModel,
createDDSFuzzSuite,
registerOracle,
} from "@fluid-private/test-dds-utils";
import { FlushMode } from "@fluidframework/runtime-definitions/internal";

import { DirectoryFactory } from "../../index.js";
import { SharedDirectoryOracle } from "../directoryOracle.js";

import { assertEquivalentDirectories } from "./directoryEquivalenceUtils.js";
import { _dirname } from "./dirname.cjs";
Expand All @@ -21,6 +28,16 @@ import {
type DirOperation,
type DirOperationGenerationConfig,
} from "./fuzzUtils.js";
import { hasSharedDirectroyOracle, type ISharedDirectoryWithOracle } from "./oracleUtils.js";

const oracleEmitter = new TypedEventEmitter<DDSFuzzHarnessEvents>();

oracleEmitter.on("clientCreate", (client) => {
const channel = client.channel as ISharedDirectoryWithOracle;
const directroyOracle = new SharedDirectoryOracle(channel);
channel.sharedDirectoryOracle = directroyOracle;
registerOracle(directroyOracle);
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

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

Variable name has a typo: 'directroyOracle' should be 'directoryOracle'

Suggested change
const directroyOracle = new SharedDirectoryOracle(channel);
channel.sharedDirectoryOracle = directroyOracle;
registerOracle(directroyOracle);
const directoryOracle = new SharedDirectoryOracle(channel);
channel.sharedDirectoryOracle = directoryOracle;
registerOracle(directoryOracle);

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

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

Variable reference has a typo: 'directroyOracle' should be 'directoryOracle'

Suggested change
const directroyOracle = new SharedDirectoryOracle(channel);
channel.sharedDirectoryOracle = directroyOracle;
registerOracle(directroyOracle);
const directoryOracle = new SharedDirectoryOracle(channel);
channel.sharedDirectoryOracle = directoryOracle;
registerOracle(directoryOracle);

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

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

Variable reference has a typo: 'directroyOracle' should be 'directoryOracle'

Suggested change
const directroyOracle = new SharedDirectoryOracle(channel);
channel.sharedDirectoryOracle = directroyOracle;
registerOracle(directroyOracle);
const directoryOracle = new SharedDirectoryOracle(channel);
channel.sharedDirectoryOracle = directoryOracle;
registerOracle(directoryOracle);

Copilot uses AI. Check for mistakes.
});

describe("SharedDirectory fuzz Create/Delete concentrated", () => {
const options: DirOperationGenerationConfig = {
Expand All @@ -37,7 +54,16 @@ describe("SharedDirectory fuzz Create/Delete concentrated", () => {
workloadName: "default directory 1",
generatorFactory: () => takeAsync(100, makeDirOperationGenerator(options)),
reducer: makeDirReducer({ clientIds: ["A", "B", "C"], printConsoleLogs: false }),
validateConsistency: async (a, b) => assertEquivalentDirectories(a.channel, b.channel),
validateConsistency: async (a, b) => {
if (hasSharedDirectroyOracle(a.channel)) {
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

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

Function call has a typo: 'hasSharedDirectroyOracle' should be 'hasSharedDirectoryOracle'

Copilot uses AI. Check for mistakes.
a.channel.sharedDirectoryOracle.validate();
}

if (hasSharedDirectroyOracle(b.channel)) {
b.channel.sharedDirectoryOracle.validate();
}
return assertEquivalentDirectories(a.channel, b.channel);
},
factory: new DirectoryFactory(),
};

Expand All @@ -56,6 +82,7 @@ describe("SharedDirectory fuzz Create/Delete concentrated", () => {
stashableClientProbability: 0.2,
},
defaultTestCount: 25,
emitter: oracleEmitter,
// Uncomment this line to replay a specific seed from its failure file:
// replay: 21,
saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/1") },
Expand Down Expand Up @@ -83,6 +110,7 @@ describe("SharedDirectory fuzz Create/Delete concentrated", () => {
stashableClientProbability: undefined,
},
defaultTestCount: 200,
emitter: oracleEmitter,
// Uncomment this line to replay a specific seed from its failure file:
// replay: 0,
saveFailures: {
Expand All @@ -108,6 +136,7 @@ describe("SharedDirectory fuzz", () => {
stashableClientProbability: 0.2,
},
defaultTestCount: 25,
emitter: oracleEmitter,
// Uncomment this line to replay a specific seed from its failure file:
// replay: 0,
saveFailures: { directory: dirPath.join(_dirname, "../../../src/test/mocha/results/2") },
Expand Down Expand Up @@ -136,6 +165,7 @@ describe("SharedDirectory fuzz", () => {
stashableClientProbability: undefined,
},
defaultTestCount: 200,
emitter: oracleEmitter,
// Uncomment this line to replay a specific seed from its failure file:
// replay: 0,
saveFailures: {
Expand Down
13 changes: 11 additions & 2 deletions packages/dds/map/src/test/mocha/fuzzUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from "../../index.js";

import { assertEquivalentDirectories } from "./directoryEquivalenceUtils.js";
import { hasSharedMapOracle } from "./oracleUtils.js";
import { hasSharedMapOracle, hasSharedDirectroyOracle } from "./oracleUtils.js";

/**
* Represents a map clear operation.
Expand Down Expand Up @@ -518,7 +518,16 @@ export const baseDirModel: DDSFuzzModel<DirectoryFactory, DirOperation> = {
workloadName: "default directory 1",
generatorFactory: () => takeAsync(100, makeDirOperationGenerator(dirDefaultOptions)),
reducer: makeDirReducer({ clientIds: ["A", "B", "C"], printConsoleLogs: false }),
validateConsistency: async (a, b) => assertEquivalentDirectories(a.channel, b.channel),
validateConsistency: async (a, b) => {
if (hasSharedDirectroyOracle(a.channel)) {
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

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

Function call has a typo: 'hasSharedDirectroyOracle' should be 'hasSharedDirectoryOracle'

Copilot uses AI. Check for mistakes.
a.channel.sharedDirectoryOracle.validate();
}

if (hasSharedDirectroyOracle(b.channel)) {
b.channel.sharedDirectoryOracle.validate();
}
return assertEquivalentDirectories(a.channel, b.channel);
},
factory: new DirectoryFactory(),
minimizationTransforms: [
(op: DirOperation): void => {
Expand Down
19 changes: 19 additions & 0 deletions packages/dds/map/src/test/mocha/oracleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,36 @@
* Licensed under the MIT License.
*/

import type { SharedDirectory } from "../../directoryFactory.js";
import type { ISharedMap } from "../../interfaces.js";
import type { SharedMap } from "../../mapFactory.js";
import { SharedDirectoryOracle } from "../directoryOracle.js";
import { SharedMapOracle } from "../mapOracle.js";

/**
* @internal
*/
export interface ISharedDirectoryWithOracle extends SharedDirectory {
sharedDirectoryOracle: SharedDirectoryOracle;
}

/**
* @internal
*/
export interface ISharedMapWithOracle extends ISharedMap {
sharedMapOracle: SharedMapOracle;
}

/**
* Type guard for directory
* @internal
*/
export function hasSharedDirectroyOracle(s: SharedDirectory): s is ISharedDirectoryWithOracle {
return (
"sharedDirectoryOracle" in s && s.sharedDirectoryOracle instanceof SharedDirectoryOracle
);
}

/**
* Type guard for map
* @internal
Expand Down
Loading