import type { SyncSettings } from "sync-client"; import { MockAgent } from "./agent/mock-agent"; import { sleep } from "./utils/sleep"; import { v4 as uuidv4 } from "uuid"; import { randomCasing } from "./utils/random-casing"; const TEST_ITERATIONS = 5; // Simulate async file access by injecting waiting time before returning from file operations. let slowFileEvents = false; async function runTest({ agentCount, concurrency, iterations, doDeletes, doResets, useSlowFileEvents, jitterScaleInSeconds }: { agentCount: number; concurrency: number; iterations: number; doDeletes: boolean; doResets: boolean; useSlowFileEvents: boolean; jitterScaleInSeconds: number; }): Promise { slowFileEvents = useSlowFileEvents; const settings = `with ${agentCount} agents, concurrency ${concurrency}, iterations ${iterations}, doDeletes ${doDeletes}, doResets ${doResets}, jitterScaleInSeconds ${jitterScaleInSeconds}, useSlowFileEvents ${useSlowFileEvents}`; console.info(`Running test ${settings}`); const vaultName = uuidv4(); console.info(`Using vault name: ${vaultName}`); const initialSettings: Partial = { isSyncEnabled: true, token: " test-token-change-me ", // same as in sync-server/config-e2e.yml with spaces vaultName: randomCasing(vaultName) + (Math.random() > 0.5 ? " " : ""), // extra spaces shouldn't matter syncConcurrency: concurrency, remoteUri: "http://localhost:3000" }; const clients: MockAgent[] = []; for (let i = 0; i < agentCount; i++) { clients.push( new MockAgent( initialSettings, `agent-${i}`, doDeletes, doResets, useSlowFileEvents, jitterScaleInSeconds ) ); } try { // eslint-disable-next-line no-restricted-properties await Promise.all(clients.map(async (client) => client.init())); for (let i = 0; i < iterations; i++) { console.info(`Iteration ${i + 1}/${iterations}`); // eslint-disable-next-line no-restricted-properties await Promise.all(clients.map(async (client) => client.act())); await sleep(100); } console.info("Stopping agents"); // Each agent can have unpushed changes which might conflict with eachother so each has to resolve the conflicts & push, and for (const client of clients) { try { await client.finish(); } catch (err) { if (!slowFileEvents) { throw err; } } } // then we need a second pass to ensure that all agents pull the same state. for (const client of clients) { try { await client.destroy(); } catch (err) { if (!slowFileEvents) { throw err; } } } console.info("Agents finished successfully"); clients.slice(0, -1).forEach((client, i) => { console.info( `Checking consistency between ${client.name} and ${clients[i + 1].name}` ); client.assertFileSystemsAreConsistent(clients[i]); console.info(`Consistency check for ${client.name} passed`); }); console.info("File systems found to be consistent"); clients.forEach((client) => { console.info(`Checking content for ${client.name}`); client.assertAllContentIsPresentOnce(); console.info(`Content check for ${client.name} passed`); }); console.info(`Test passed ${settings}`); } catch (err) { console.error(`Test failed ${settings}`); throw err; } } async function runTests(): Promise { for (let i = 0; i < TEST_ITERATIONS; i++) { await runTest({ agentCount: 2, concurrency: 16, iterations: 100, doDeletes: true, doResets: true, useSlowFileEvents: true, jitterScaleInSeconds: 0.75 }); for (const useSlowFileEvents of [false, true]) { for (const concurrency of [ 16, 1 // test with concurrency 1 to check for deadlocks ]) { for (const doDeletes of [true, false]) { await runTest({ agentCount: 2, concurrency, iterations: 100, doDeletes, doResets: false, useSlowFileEvents, jitterScaleInSeconds: 0.75 }); } } } } } process.on("uncaughtException", (error) => { if (slowFileEvents) { return; } if ( error instanceof Error && error.message.includes( "WebSocket was closed before the connection was established" ) ) { return; } console.error("Uncaught exception:", error); process.exit(1); }); process.on("unhandledRejection", (error, _promise) => { if (error instanceof Error && error.message === "Sync was reset") { return; } if (slowFileEvents) { return; } console.error("Unhandled rejection:", error); process.exit(1); }); runTests() .then(() => { process.exit(0); }) .catch((err: unknown) => { console.error(err); process.exit(1); });